io.deephaven.time.calendar.BusinessCalendar Maven / Gradle / Ivy
Show all versions of deephaven-engine-time Show documentation
//
// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
//
package io.deephaven.time.calendar;
import io.deephaven.base.verify.Require;
import io.deephaven.base.verify.RequirementFailure;
import io.deephaven.time.DateTimeUtils;
import io.deephaven.util.QueryConstants;
import java.time.*;
import java.util.*;
import static io.deephaven.util.QueryConstants.*;
/**
* A business calendar, with the concept of business and non-business time.
*
* Date strings must be in a format that can be parsed by {@code DateTimeUtils#parseDate}. Methods that accept strings
* can be slower than methods written explicitly for {@code Instant}, {@code ZonedDateTime}, or {@code LocalDate}.
*/
public class BusinessCalendar extends Calendar {
private final LocalDate firstValidDate;
private final LocalDate lastValidDate;
private final CalendarDay standardBusinessDay;
private final Set weekendDays;
private final Map> holidays;
// region Exceptions
/**
* A runtime exception that is thrown when a date is invalid.
*/
public static class InvalidDateException extends RuntimeException {
/**
* Creates a new exception.
*
* @param message exception message.
*/
private InvalidDateException(final String message) {
super(message);
}
/**
* Creates a new exception.
*
* @param message exception message.
* @param cause cause of the exception.
*/
public InvalidDateException(final String message, final Throwable cause) {
super(message, cause);
}
}
// endregion
// region Cache
private final Map> cachedSchedules = new HashMap<>();
private final Map cachedYearData = new HashMap<>();
private void populateSchedules() {
LocalDate date = firstValidDate;
while (!date.isAfter(lastValidDate)) {
final CalendarDay s = holidays.get(date);
if (s != null) {
cachedSchedules.put(date, s);
} else if (weekendDays.contains(date.getDayOfWeek())) {
cachedSchedules.put(date, CalendarDay.toInstant(CalendarDay.HOLIDAY, date, timeZone()));
} else {
cachedSchedules.put(date, CalendarDay.toInstant(standardBusinessDay, date, timeZone()));
}
date = date.plusDays(1);
}
}
private static class YearData {
private final Instant start;
private final Instant end;
private final long businessTimeNanos;
public YearData(final Instant start, final Instant end, final long businessTimeNanos) {
this.start = start;
this.end = end;
this.businessTimeNanos = businessTimeNanos;
}
}
private void populateCachedYearData() {
// Only cache complete years, since incomplete years can not be fully computed.
final int yearStart =
firstValidDate.getDayOfYear() == 1 ? firstValidDate.getYear() : firstValidDate.getYear() + 1;
final int yearEnd = ((lastValidDate.isLeapYear() && lastValidDate.getDayOfYear() == 366)
|| lastValidDate.getDayOfYear() == 365) ? lastValidDate.getYear() : lastValidDate.getYear() - 1;
for (int year = yearStart; year <= yearEnd; year++) {
final LocalDate startDate = LocalDate.ofYearDay(year, 1);
final LocalDate endDate = LocalDate.ofYearDay(year + 1, 1);
final ZonedDateTime start = startDate.atTime(0, 0).atZone(timeZone());
final ZonedDateTime end = endDate.atTime(0, 0).atZone(timeZone());
LocalDate date = startDate;
long businessTimeNanos = 0;
while (date.isBefore(endDate)) {
final CalendarDay bs = this.calendarDay(date);
businessTimeNanos += bs.businessNanos();
date = date.plusDays(1);
}
final YearData yd = new YearData(start.toInstant(), end.toInstant(), businessTimeNanos);
cachedYearData.put(year, yd);
}
}
private YearData getYearData(final int year) {
final YearData yd = cachedYearData.get(year);
if (yd == null) {
throw new InvalidDateException("Business calendar does not contain a complete year for: year=" + year);
}
return yd;
}
// endregion
// region Constructors
/**
* Creates a new business calendar.
*
* @param name calendar name.
* @param description calendar description.
* @param timeZone calendar time zone.
* @param firstValidDate first valid date for the business calendar.
* @param lastValidDate last valid date for the business calendar.
* @param standardBusinessDay business day schedule for a standard business day
* @param weekendDays weekend days
* @param holidays holidays. Business day schedules for all holidays. A holiday is a date that has a schedule that
* is different from the schedule for a standard business day or weekend.
* @throws RequirementFailure if any argument is null.
*/
public BusinessCalendar(final String name, final String description, final ZoneId timeZone,
final LocalDate firstValidDate, final LocalDate lastValidDate,
final CalendarDay standardBusinessDay, final Set weekendDays,
final Map> holidays) {
super(name, description, timeZone);
this.firstValidDate = Require.neqNull(firstValidDate, "firstValidDate");
this.lastValidDate = Require.neqNull(lastValidDate, "lastValidDate");
this.standardBusinessDay = Require.neqNull(standardBusinessDay, "standardBusinessDay");
this.weekendDays = Set.copyOf(Require.neqNull(weekendDays, "weekendDays"));
this.holidays = Map.copyOf(Require.neqNull(holidays, "holidays"));
populateSchedules();
populateCachedYearData();
}
// endregion
// region Getters
/**
* Returns the first valid date for the business calendar.
*
* @return first valid date for the business calendar.
*/
public LocalDate firstValidDate() {
return firstValidDate;
}
/**
* Returns the last valid date for the business calendar.
*
* @return last valid date for the business calendar.
*/
public LocalDate lastValidDate() {
return lastValidDate;
}
// endregion
// region Business Schedule
/**
* Returns the days that make up a weekend.
*
* @return days that make up a weekend.
*/
public Set weekendDays() {
return this.weekendDays;
}
/**
* Business day schedule for a standard business day.
*
* @return business day schedule for a standard business day.
*/
public CalendarDay standardBusinessDay() {
return standardBusinessDay;
}
/**
* Length of a standard business day in nanoseconds.
*
* @return length of a standard business day in nanoseconds
*/
public long standardBusinessNanos() {
return standardBusinessDay.businessNanos();
}
/**
* Length of a standard business day.
*
* @return length of a standard business day
*/
public Duration standardBusinessDuration() {
return standardBusinessDay.businessDuration();
}
/**
* Business day schedules for all holidays. A holiday is a date that has a schedule that is different from the
* schedule for a standard business day or weekend.
*
* @return a map of holiday dates and their calendar days
*/
public Map> holidays() {
return holidays;
}
/**
* Returns the {@link CalendarDay} for a date.
*
* @param date date
* @return the corresponding {@link CalendarDay} of {@code date}. {@code null} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public CalendarDay calendarDay(final LocalDate date) {
if (date == null) {
return null;
}
if (date.isBefore(firstValidDate)) {
throw new InvalidDateException("Date is before the first valid business calendar date: date=" + date
+ " firstValidDate=" + firstValidDate);
} else if (date.isAfter(lastValidDate)) {
throw new InvalidDateException("Date is after the last valid business calendar date: date=" + date
+ " lastValidDate=" + lastValidDate);
}
return cachedSchedules.get(date);
}
/**
* Returns the {@link CalendarDay} for a date.
*
* @param time time
* @return the corresponding {@link CalendarDay} of {@code date}. {@code null} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public CalendarDay calendarDay(final ZonedDateTime time) {
if (time == null) {
return null;
}
return calendarDay(time.withZoneSameInstant(timeZone()).toLocalDate());
}
/**
* Returns the {@link CalendarDay} for a date.
*
* @param time time
* @return the corresponding {@link CalendarDay} of {@code date}. {@code null} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public CalendarDay calendarDay(final Instant time) {
if (time == null) {
return null;
}
return calendarDay(time.atZone(timeZone()));
}
/**
* Returns the {@link CalendarDay} for a date.
*
* @param date date
* @return the corresponding {@link CalendarDay} of {@code date}. {@code null} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public CalendarDay calendarDay(final String date) {
if (date == null) {
return null;
}
return this.calendarDay(DateTimeUtils.parseLocalDate(date));
}
/**
* Returns the {@link CalendarDay} for a date.
*
* @return today's business day schedule
* @throws InvalidDateException if the date is not in the valid range
*/
public CalendarDay calendarDay() {
return this.calendarDay(calendarDate());
}
// endregion
// region Business Day
/**
* Is the date a business day?
*
* @param date date
* @return true if the date is a business day; false otherwise. False if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isBusinessDay(final LocalDate date) {
if (date == null) {
return false;
}
return this.calendarDay(date).isBusinessDay();
}
/**
* Is the date a business day?
*
* @param date date
* @return true if the date is a business day; false otherwise. False if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public boolean isBusinessDay(final String date) {
if (date == null) {
return false;
}
return calendarDay(date).isBusinessDay();
}
/**
* Is the time on a business day?
*
* As long as the time occurs on a business day, it is considered a business day. The time does not have to be
* within the business day schedule. To determine if a time is within the business day schedule, use
* {@link #isBusinessTime(ZonedDateTime)}.
*
* @param time time
* @return true if the date is a business day; false otherwise. False if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isBusinessDay(final ZonedDateTime time) {
if (time == null) {
return false;
}
return this.calendarDay(time).isBusinessDay();
}
/**
* Is the time on a business day?
*
* As long as the time occurs on a business day, it is considered a business day. The time does not have to be
* within the business day schedule. To determine if a time is within the business day schedule, use
* {@link #isBusinessTime(Instant)}.
*
* @param time time
* @return true if the date is a business day; false otherwise. False if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isBusinessDay(final Instant time) {
if (time == null) {
return false;
}
return this.calendarDay(time).isBusinessDay();
}
/**
* Is the day of the week a normal business day?
*
* @param day a day of the week
* @return true if the day is a business day; false otherwise. False if the input is {@code null}.
*/
public boolean isBusinessDay(final DayOfWeek day) {
if (day == null) {
return false;
}
return !weekendDays.contains(day);
}
/**
* Is the current day a business day? As long as the current time occurs on a business day, it is considered a
* business day. The time does not have to be within the business day schedule.
*
* @return true if the current day is a business day; false otherwise
*/
public boolean isBusinessDay() {
return isBusinessDay(calendarDate());
}
/**
* Is the date the last business day of the month?
*
* @param date date
* @return true if {@code date} is the last business day of the month; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
boolean isLastBusinessDayOfMonth(final LocalDate date) {
if (date == null || !isBusinessDay(date)) {
return false;
}
final LocalDate nextBusAfterDate = plusBusinessDays(date, 1);
assert nextBusAfterDate != null;
return date.getMonth() != nextBusAfterDate.getMonth();
}
/**
* Is the time on the last business day of the month?
*
* As long as the time occurs on a business day, it is considered a business day. The time does not have to be
* within the business day schedule.
*
* @param time time
* @return true if {@code time} is on the last business day of the month; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isLastBusinessDayOfMonth(final ZonedDateTime time) {
if (time == null) {
return false;
}
Require.neqNull(time, "time");
return isLastBusinessDayOfMonth(DateTimeUtils.toLocalDate(time.withZoneSameInstant(timeZone())));
}
/**
* Is the time on the last business day of the month?
*
* As long as the time occurs on a business day, it is considered a business day. The time does not have to be
* within the business day schedule.
*
* @param time time
* @return true if {@code time} is on the last business day of the month; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isLastBusinessDayOfMonth(final Instant time) {
if (time == null) {
return false;
}
return isLastBusinessDayOfMonth(DateTimeUtils.toLocalDate(time, timeZone()));
}
/**
* Is the date the last business day of the month?
*
* @param date date
* @return true if {@code time} is on the last business day of the month; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public boolean isLastBusinessDayOfMonth(final String date) {
if (date == null) {
return false;
}
return isLastBusinessDayOfMonth(DateTimeUtils.parseLocalDate(date));
}
/**
* Is the current date the last business day of the month?
*
* @return true if the current date is the last business day of the month; false otherwise.
*/
public boolean isLastBusinessDayOfMonth() {
return isLastBusinessDayOfMonth(calendarDate());
}
/**
* Is the date the last business day of the week?
*
* @param date date
* @return true if {@code date} is on the last business day of the week; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isLastBusinessDayOfWeek(final LocalDate date) {
if (date == null || !isBusinessDay(date)) {
return false;
}
final LocalDate nextBusinessDay = plusBusinessDays(date, 1);
return date.getDayOfWeek().compareTo(nextBusinessDay.getDayOfWeek()) > 0
|| numberCalendarDates(date, nextBusinessDay) > 6;
}
/**
* Is the time on the last business day of the week?
*
* As long as the time occurs on a business day, it is considered a business day. The time does not have to be
* within the business day schedule.
*
* @param time time
* @return true if {@code time} is on the last business day of the week; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isLastBusinessDayOfWeek(final ZonedDateTime time) {
if (time == null) {
return false;
}
return isLastBusinessDayOfWeek(time.withZoneSameInstant(timeZone()).toLocalDate());
}
/**
* Is the time on the last business day of the week?
*
* As long as the time occurs on a business day, it is considered a business day. The time does not have to be
* within the business day schedule.
*
* @param time time
* @return true if {@code time} is on the last business day of the week; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isLastBusinessDayOfWeek(final Instant time) {
if (time == null) {
return false;
}
return isLastBusinessDayOfWeek(DateTimeUtils.toLocalDate(time, timeZone()));
}
/**
* Is the date is last business day of the week?
*
* @param date date
* @return true if {@code date} is the last business day of the week; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public boolean isLastBusinessDayOfWeek(final String date) {
if (date == null) {
return false;
}
return isLastBusinessDayOfWeek(DateTimeUtils.parseLocalDate(date));
}
/**
* Is the current date the last business day of the week?
*
* @return true if the current date is the last business day of the week; false otherwise.
*/
public boolean isLastBusinessDayOfWeek() {
return isLastBusinessDayOfWeek(calendarDate());
}
/**
* Is the date the last business day of the year?
*
* @param date date
* @return true if {@code date} is the last business day of the year; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
boolean isLastBusinessDayOfYear(final LocalDate date) {
if (date == null || !isBusinessDay(date)) {
return false;
}
final LocalDate nextBusAfterDate = plusBusinessDays(date, 1);
assert nextBusAfterDate != null;
return date.getYear() != nextBusAfterDate.getYear();
}
/**
* Is the time on the last business day of the year?
*
* As long as the time occurs on a business day, it is considered a business day. The time does not have to be
* within the business day schedule.
*
* @param time time
* @return true if {@code time} is on the last business day of the year; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isLastBusinessDayOfYear(final ZonedDateTime time) {
if (time == null) {
return false;
}
return isLastBusinessDayOfYear(DateTimeUtils.toLocalDate(time.withZoneSameInstant(timeZone())));
}
/**
* Is the time on the last business day of the year?
*
* As long as the time occurs on a business day, it is considered a business day. The time does not have to be
* within the business day schedule.
*
* @param time time
* @return true if {@code time} is on the last business day of the year; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isLastBusinessDayOfYear(final Instant time) {
if (time == null) {
return false;
}
return isLastBusinessDayOfYear(DateTimeUtils.toLocalDate(time, timeZone()));
}
/**
* Is the date the last business day of the year?
*
* @param date date
* @return true if {@code time} is on the last business day of the year; false otherwise. False if the input is
* {@code null}.
* @throws InvalidDateException if the date is not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
boolean isLastBusinessDayOfYear(final String date) {
if (date == null) {
return false;
}
Require.neqNull(date, "date");
return isLastBusinessDayOfYear(DateTimeUtils.parseLocalDate(date));
}
/**
* Is the current date the last business day of the year?
*
* As long as the current time occurs on a business day, it is considered a business day. The time does not have to
* be within the business day schedule.
*
* @return true if the current date is the last business day of the year; false otherwise.
*/
public boolean isLastBusinessDayOfYear() {
return isLastBusinessDayOfYear(calendarDate());
}
// endregion
// region Business Time
/**
* Determines if the specified time is a business time. Business times fall within business time ranges of the day's
* business schedule.
*
* @param time time
* @return true if the specified time is a business time; otherwise, false. False if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isBusinessTime(final ZonedDateTime time) {
if (time == null) {
return false;
}
return this.calendarDay(time).isBusinessTime(time.toInstant());
}
/**
* Determines if the specified time is a business time. Business times fall within business time ranges of the day's
* business schedule.
*
* @param time time
* @return true if the specified time is a business time; otherwise, false. False if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isBusinessTime(final Instant time) {
if (time == null) {
return false;
}
return this.calendarDay(time).isBusinessTime(time);
}
/**
* Determines if the current time according to the Deephaven system clock is a business time. Business times fall
* within business time ranges of the day's business schedule.
*
* @return true if the specified time is a business time; otherwise, false.
* @throws InvalidDateException if the date is not in the valid range
*/
public boolean isBusinessTime() {
return isBusinessTime(DateTimeUtils.now());
}
/**
* Returns the ratio of the business day length and the standard business day length. For example, a holiday has
* zero business time and will therefore return 0.0. A normal business day will be of the standard length and will
* therefore return 1.0. A NYSE half day holiday will return 0.538 (3.5 hours open, over a standard 6.5 hour day).
*
* @param date date
* @return ratio of the business day length and the standard business day length for the date.
* {@link io.deephaven.util.QueryConstants#NULL_DOUBLE} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public double fractionStandardBusinessDay(final LocalDate date) {
if (date == null) {
return NULL_DOUBLE;
}
final CalendarDay schedule = this.calendarDay(date);
return (double) schedule.businessNanos() / (double) standardBusinessNanos();
}
/**
* Returns the ratio of the business day length and the standard business day length. For example, a holiday has
* zero business time and will therefore return 0.0. A normal business day will be of the standard length and will
* therefore return 1.0. A half day holiday will return 0.5.
*
* @param date date
* @return ratio of the business day length and the standard business day length for the date.
* {@link io.deephaven.util.QueryConstants#NULL_DOUBLE} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public double fractionStandardBusinessDay(final String date) {
if (date == null) {
return NULL_DOUBLE;
}
return fractionStandardBusinessDay(DateTimeUtils.parseLocalDate(date));
}
/**
* Returns the ratio of the business day length and the standard business day length. For example, a holiday has
* zero business time and will therefore return 0.0. A normal business day will be of the standard length and will
* therefore return 1.0. A half day holiday will return 0.5.
*
* @param time time
* @return ratio of the business day length and the standard business day length for the date.
* {@link io.deephaven.util.QueryConstants#NULL_DOUBLE} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public double fractionStandardBusinessDay(final Instant time) {
if (time == null) {
return NULL_DOUBLE;
}
return fractionStandardBusinessDay(DateTimeUtils.toLocalDate(time, timeZone()));
}
/**
* Returns the ratio of the business day length and the standard business day length. For example, a holiday has
* zero business time and will therefore return 0.0. A normal business day will be of the standard length and will
* therefore return 1.0. A half day holiday will return 0.5.
*
* @param time time
* @return ratio of the business day length and the standard business day length for the date.
* {@link io.deephaven.util.QueryConstants#NULL_DOUBLE} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public double fractionStandardBusinessDay(final ZonedDateTime time) {
if (time == null) {
return NULL_DOUBLE;
}
return fractionStandardBusinessDay(DateTimeUtils.toLocalDate(time.toInstant(), timeZone()));
}
/**
* Returns the ratio of the business day length and the standard business day length. For example, a holiday has
* zero business time and will therefore return 0.0. A normal business day will be of the standard length and will
* therefore return 1.0. A half day holiday will return 0.5.
*
* @return ratio of the business day length and the standard business day length for the date
* @throws InvalidDateException if the date is not in the valid range
*/
public double fractionStandardBusinessDay() {
return fractionStandardBusinessDay(calendarDate());
}
/**
* Fraction of the business day complete.
*
* @param time time
* @return the fraction of the business day complete, or 1.0 if the day is not a business day.
* {@link io.deephaven.util.QueryConstants#NULL_DOUBLE} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public double fractionBusinessDayComplete(final Instant time) {
if (time == null) {
return NULL_DOUBLE;
}
final CalendarDay schedule = this.calendarDay(time);
if (!schedule.isBusinessDay()) {
return 1.0;
}
final long businessDaySoFar = schedule.businessNanosElapsed(time);
return (double) businessDaySoFar / (double) schedule.businessNanos();
}
/**
* Fraction of the business day complete.
*
* @param time time
* @return the fraction of the business day complete, or 1.0 if the day is not a business day.
* {@link io.deephaven.util.QueryConstants#NULL_DOUBLE} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public double fractionBusinessDayComplete(final ZonedDateTime time) {
if (time == null) {
return NULL_DOUBLE;
}
return fractionBusinessDayComplete(time.toInstant());
}
/**
* Fraction of the current business day complete.
*
* @return the fraction of the business day complete, or 1.0 if the day is not a business day
* @throws InvalidDateException if the date is not in the valid range
*/
public double fractionBusinessDayComplete() {
return fractionBusinessDayComplete(DateTimeUtils.now());
}
/**
* Fraction of the business day remaining.
*
* @param time time
* @return the fraction of the business day complete, or 0.0 if the day is not a business
* day.{@link io.deephaven.util.QueryConstants#NULL_DOUBLE} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public double fractionBusinessDayRemaining(final Instant time) {
if (time == null) {
return NULL_DOUBLE;
}
return 1.0 - fractionBusinessDayComplete(time);
}
/**
* Fraction of the business day remaining.
*
* @param time time
* @return the fraction of the business day complete, or 0.0 if the day is not a business day.
* {@link io.deephaven.util.QueryConstants#NULL_DOUBLE} if the input is {@code null}.
* @throws InvalidDateException if the date is not in the valid range
*/
public double fractionBusinessDayRemaining(final ZonedDateTime time) {
if (time == null) {
return NULL_DOUBLE;
}
return 1.0 - fractionBusinessDayComplete(time);
}
/**
* Fraction of the business day remaining.
*
* @return the fraction of the business day complete, or 0.0 if the day is not a business day
* @throws InvalidDateException if the date is not in the valid range
*/
public double fractionBusinessDayRemaining() {
return fractionBusinessDayRemaining(DateTimeUtils.now());
}
// endregion
// region Ranges
/**
* Returns the number of business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return number of business dates between {@code start} and {@code end}. {@link QueryConstants#NULL_INT} if any
* input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberBusinessDates(final LocalDate start, final LocalDate end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return NULL_INT;
}
int days = 0;
for (LocalDate day = start; !day.isAfter(end); day = day.plusDays(1)) {
final boolean skip = (!startInclusive && day.equals(start)) || (!endInclusive && day.equals(end));
if (!skip && isBusinessDay(day)) {
days++;
}
}
return days;
}
/**
* Returns the number of business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return number of business dates between {@code start} and {@code end}. {@link QueryConstants#NULL_INT} if any
* input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public int numberBusinessDates(final String start, final String end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return NULL_INT;
}
return numberBusinessDates(DateTimeUtils.parseLocalDate(start), DateTimeUtils.parseLocalDate(end),
startInclusive, endInclusive);
}
/**
* Returns the number of business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return number of business dates between {@code start} and {@code end}. {@link QueryConstants#NULL_INT} if any
* input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberBusinessDates(final ZonedDateTime start, final ZonedDateTime end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return NULL_INT;
}
return numberBusinessDates(start.withZoneSameInstant(timeZone()).toLocalDate(),
end.withZoneSameInstant(timeZone()).toLocalDate(), startInclusive, endInclusive);
}
/**
* Returns the number of business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return number of business dates between {@code start} and {@code end}. {@link QueryConstants#NULL_INT} if any
* input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberBusinessDates(final Instant start, final Instant end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return NULL_INT;
}
return numberBusinessDates(DateTimeUtils.toLocalDate(start, timeZone()),
DateTimeUtils.toLocalDate(end, timeZone()), startInclusive, endInclusive);
}
/**
* Returns the number of business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return number of business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@link QueryConstants#NULL_INT} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberBusinessDates(final LocalDate start, final LocalDate end) {
return numberBusinessDates(start, end, true, true);
}
/**
* Returns the number of business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return number of business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@link QueryConstants#NULL_INT} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public int numberBusinessDates(final String start, final String end) {
return numberBusinessDates(start, end, true, true);
}
/**
* Returns the number of business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return number of business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@link QueryConstants#NULL_INT} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberBusinessDates(final ZonedDateTime start, final ZonedDateTime end) {
return numberBusinessDates(start, end, true, true);
}
/**
* Returns the number of business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return number of business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@link QueryConstants#NULL_INT} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberBusinessDates(final Instant start, final Instant end) {
return numberBusinessDates(start, end, true, true);
}
/**
* Returns the number of non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return number of non-business dates between {@code start} and {@code end}. {@link QueryConstants#NULL_INT} if
* any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberNonBusinessDates(final LocalDate start, final LocalDate end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return NULL_INT;
}
return numberCalendarDates(start, end, startInclusive, endInclusive)
- numberBusinessDates(start, end, startInclusive, endInclusive);
}
/**
* Returns the number of non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return number of non-business dates between {@code start} and {@code end}. {@link QueryConstants#NULL_INT} if
* any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public int numberNonBusinessDates(final String start, final String end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return NULL_INT;
}
return numberNonBusinessDates(DateTimeUtils.parseLocalDate(start), DateTimeUtils.parseLocalDate(end),
startInclusive, endInclusive);
}
/**
* Returns the number of non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return number of non-business dates between {@code start} and {@code end}. {@link QueryConstants#NULL_INT} if
* any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberNonBusinessDates(final ZonedDateTime start, final ZonedDateTime end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return NULL_INT;
}
return numberNonBusinessDates(start.withZoneSameInstant(timeZone()).toLocalDate(),
end.withZoneSameInstant(timeZone()).toLocalDate(), startInclusive, endInclusive);
}
/**
* Returns the number of non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return number of non-business dates between {@code start} and {@code end}. {@link QueryConstants#NULL_INT} if
* any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberNonBusinessDates(final Instant start, final Instant end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return NULL_INT;
}
return numberNonBusinessDates(DateTimeUtils.toLocalDate(start, timeZone()),
DateTimeUtils.toLocalDate(end, timeZone()), startInclusive, endInclusive);
}
/**
* Returns the number of non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return number of non-business dates between {@code start} and {@code end}; including {@code start} and
* {@code end}. {@link QueryConstants#NULL_INT} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberNonBusinessDates(final LocalDate start, final LocalDate end) {
return numberNonBusinessDates(start, end, true, true);
}
/**
* Returns the number of non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return number of non-business dates between {@code start} and {@code end}; including {@code start} and
* {@code end}. {@link QueryConstants#NULL_INT} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public int numberNonBusinessDates(final String start, final String end) {
return numberNonBusinessDates(start, end, true, true);
}
/**
* Returns the number of non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return number of non-business dates between {@code start} and {@code end}; including {@code start} and
* {@code end}. {@link QueryConstants#NULL_INT} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberNonBusinessDates(final ZonedDateTime start, final ZonedDateTime end) {
return numberNonBusinessDates(start, end, true, true);
}
/**
* Returns the number of non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return number of non-business dates between {@code start} and {@code end}; including {@code start} and
* {@code end}. {@link QueryConstants#NULL_INT} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public int numberNonBusinessDates(final Instant start, final Instant end) {
return numberNonBusinessDates(start, end, true, true);
}
/**
* Returns the business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return business dates between {@code start} and {@code end}. {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] businessDates(final LocalDate start, final LocalDate end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return null;
}
List dateList = new ArrayList<>();
for (LocalDate day = start; !day.isAfter(end); day = day.plusDays(1)) {
final boolean skip = (!startInclusive && day.equals(start)) || (!endInclusive && day.equals(end));
if (!skip && isBusinessDay(day)) {
dateList.add(day);
}
}
return dateList.toArray(new LocalDate[0]);
}
/**
* Returns the business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return business dates between {@code start} and {@code end}. {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public String[] businessDates(final String start, final String end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return null;
}
final LocalDate[] dates =
businessDates(DateTimeUtils.parseLocalDate(start), DateTimeUtils.parseLocalDate(end), startInclusive,
endInclusive);
return dates == null ? null : Arrays.stream(dates).map(DateTimeUtils::formatDate).toArray(String[]::new);
}
/**
* Returns the business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return business dates between {@code start} and {@code end}. {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] businessDates(final ZonedDateTime start, final ZonedDateTime end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return null;
}
return businessDates(start.withZoneSameInstant(timeZone()).toLocalDate(),
end.withZoneSameInstant(timeZone()).toLocalDate(), startInclusive, endInclusive);
}
/**
* Returns the business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return business dates between {@code start} and {@code end}. {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] businessDates(final Instant start, final Instant end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return null;
}
return businessDates(DateTimeUtils.toLocalDate(start, timeZone()), DateTimeUtils.toLocalDate(end, timeZone()),
startInclusive, endInclusive);
}
/**
* Returns the business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] businessDates(final LocalDate start, final LocalDate end) {
return businessDates(start, end, true, true);
}
/**
* Returns the business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public String[] businessDates(final String start, final String end) {
return businessDates(start, end, true, true);
}
/**
* Returns the business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] businessDates(final ZonedDateTime start, final ZonedDateTime end) {
return businessDates(start, end, true, true);
}
/**
* Returns the business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] businessDates(final Instant start, final Instant end) {
return businessDates(start, end, true, true);
}
/**
* Returns the non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return non-business dates between {@code start} and {@code end}. {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] nonBusinessDates(final LocalDate start, final LocalDate end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return null;
}
List dateList = new ArrayList<>();
for (LocalDate day = start; !day.isAfter(end); day = day.plusDays(1)) {
final boolean skip = (!startInclusive && day.equals(start)) || (!endInclusive && day.equals(end));
if (!skip && !isBusinessDay(day)) {
dateList.add(day);
}
}
return dateList.toArray(new LocalDate[0]);
}
/**
* Returns the non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return non-business dates between {@code start} and {@code end}. {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public String[] nonBusinessDates(final String start, final String end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return null;
}
final LocalDate[] dates =
nonBusinessDates(DateTimeUtils.parseLocalDate(start), DateTimeUtils.parseLocalDate(end), startInclusive,
endInclusive);
return dates == null ? null : Arrays.stream(dates).map(DateTimeUtils::formatDate).toArray(String[]::new);
}
/**
* Returns the non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return non-business dates between {@code start} and {@code end}. {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] nonBusinessDates(final ZonedDateTime start, final ZonedDateTime end,
final boolean startInclusive, final boolean endInclusive) {
if (start == null || end == null) {
return null;
}
return nonBusinessDates(start.withZoneSameInstant(timeZone()).toLocalDate(),
end.withZoneSameInstant(timeZone()).toLocalDate(), startInclusive, endInclusive);
}
/**
* Returns the non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @param startInclusive true to include {@code start} in the result; false to exclude {@code start}
* @param endInclusive true to include {@code end} in the result; false to exclude {@code end}
* @return non-business dates between {@code start} and {@code end}. {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] nonBusinessDates(final Instant start, final Instant end, final boolean startInclusive,
final boolean endInclusive) {
if (start == null || end == null) {
return null;
}
return nonBusinessDates(DateTimeUtils.toLocalDate(start, timeZone()),
DateTimeUtils.toLocalDate(end, timeZone()), startInclusive, endInclusive);
}
/**
* Returns the non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return non-business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] nonBusinessDates(final LocalDate start, final LocalDate end) {
return nonBusinessDates(start, end, true, true);
}
/**
* Returns the non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return non-business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public String[] nonBusinessDates(final String start, final String end) {
return nonBusinessDates(start, end, true, true);
}
/**
* Returns the non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return non-business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] nonBusinessDates(final ZonedDateTime start, final ZonedDateTime end) {
return nonBusinessDates(start, end, true, true);
}
/**
* Returns the non-business dates in a given range.
*
* @param start start of a time range
* @param end end of a time range
* @return non-business dates between {@code start} and {@code end}; including {@code start} and {@code end}.
* {@code null} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public LocalDate[] nonBusinessDates(final Instant start, final Instant end) {
return nonBusinessDates(start, end, true, true);
}
// endregion
// region Differences
/**
* Returns the amount of business time in nanoseconds between two times.
*
* @param start start of a time range
* @param end end of a time range
* @return the amount of business time in nanoseconds between {@code start} and {@code end}.
* {@link QueryConstants#NULL_LONG} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public long diffBusinessNanos(final Instant start, final Instant end) {
if (start == null || end == null) {
return NULL_LONG;
}
if (DateTimeUtils.isAfter(start, end)) {
return -diffBusinessNanos(end, start);
}
final LocalDate startDate = DateTimeUtils.toLocalDate(start, timeZone());
final LocalDate endDate = DateTimeUtils.toLocalDate(end, timeZone());
assert startDate != null;
assert endDate != null;
if (startDate.equals(endDate)) {
final CalendarDay schedule = this.calendarDay(startDate);
return schedule.businessNanosElapsed(end) - schedule.businessNanosElapsed(start);
}
long rst = this.calendarDay(startDate).businessNanosRemaining(start)
+ this.calendarDay(endDate).businessNanosElapsed(end);
for (LocalDate d = startDate.plusDays(1); d.isBefore(endDate); d = d.plusDays(1)) {
rst += this.calendarDay(d).businessNanos();
}
return rst;
}
/**
* Returns the amount of business time in nanoseconds between two times.
*
* @param start start of a time range
* @param end end of a time range
* @return the amount of business time in nanoseconds between {@code start} and {@code end}.
* {@link QueryConstants#NULL_LONG} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public long diffBusinessNanos(final ZonedDateTime start, final ZonedDateTime end) {
if (start == null || end == null) {
return NULL_LONG;
}
return diffBusinessNanos(start.toInstant(), end.toInstant());
}
/**
* Returns the amount of non-business time in nanoseconds between two times.
*
* @param start start of a time range
* @param end end of a time range
* @return the amount of nonbusiness time in nanoseconds between {@code start} and {@code end}.
* {@link QueryConstants#NULL_LONG} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public long diffNonBusinessNanos(final Instant start, final Instant end) {
if (start == null || end == null) {
return NULL_LONG;
}
return DateTimeUtils.diffNanos(start, end) - diffBusinessNanos(start, end);
}
/**
* Returns the amount of non-business time in nanoseconds between two times.
*
* @param start start of a time range
* @param end end of a time range
* @return the amount of non-business time in nanoseconds between {@code start} and {@code end}.
* {@link QueryConstants#NULL_LONG} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public long diffNonBusinessNanos(final ZonedDateTime start, final ZonedDateTime end) {
if (start == null || end == null) {
return NULL_LONG;
}
return diffNonBusinessNanos(start.toInstant(), end.toInstant());
}
/**
* Returns the amount of business time between two times.
*
* @param start start of a time range
* @param end end of a time range
* @return the amount of business time between {@code start} and {@code end}. {@code null} if any input is
* {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public Duration diffBusinessDuration(final Instant start, final Instant end) {
if (start == null || end == null) {
return null;
}
return Duration.ofNanos(diffBusinessNanos(start, end));
}
/**
* Returns the amount of business time between two times.
*
* @param start start of a time range
* @param end end of a time range
* @return the amount of business time between {@code start} and {@code end}. {@code null} if any input is
* {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public Duration diffBusinessDuration(final ZonedDateTime start, final ZonedDateTime end) {
if (start == null || end == null) {
return null;
}
return Duration.ofNanos(diffBusinessNanos(start, end));
}
/**
* Returns the amount of non-business time between two times.
*
* @param start start of a time range
* @param end end of a time range
* @return the amount of non-business time between {@code start} and {@code end}. {@code null} if any input is
* {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public Duration diffNonBusinessDuration(final Instant start, final Instant end) {
if (start == null || end == null) {
return null;
}
return Duration.ofNanos(diffNonBusinessNanos(start, end));
}
/**
* Returns the amount of non-business time between two times.
*
* @param start start of a time range
* @param end end of a time range
* @return the amount of non-business time between {@code start} and {@code end}. {@code null} if any input is
* {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public Duration diffNonBusinessDuration(final ZonedDateTime start, final ZonedDateTime end) {
if (start == null || end == null) {
return null;
}
return Duration.ofNanos(diffNonBusinessNanos(start, end));
}
/**
* Returns the amount of business time in standard business days between two times.
*
* @param start start of a time range
* @param end end of a time range
* @return the amount of business time in standard business days between {@code start} and {@code end}.
* {@link QueryConstants#NULL_DOUBLE} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public double diffBusinessDays(final Instant start, final Instant end) {
if (start == null || end == null) {
return NULL_DOUBLE;
}
return (double) diffBusinessNanos(start, end) / (double) standardBusinessNanos();
}
/**
* Returns the amount of business time in standard business days between two times.
*
* @param start start of a time range
* @param end end of a time range
* @return the amount of business time in standard business days between {@code start} and {@code end}.
* {@link QueryConstants#NULL_DOUBLE} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public double diffBusinessDays(final ZonedDateTime start, final ZonedDateTime end) {
if (start == null || end == null) {
return NULL_DOUBLE;
}
return (double) diffBusinessNanos(start, end) / (double) standardBusinessNanos();
}
/**
* Returns the number of business years between {@code start} and {@code end}.
*
* @param start start; if null, return null
* @param end end; if null, return null
* @return the amount of business time in business years between the {@code start} and {@code end}.
* {@link QueryConstants#NULL_DOUBLE} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public double diffBusinessYears(final Instant start, final Instant end) {
if (start == null || end == null) {
return NULL_DOUBLE;
}
final int yearStart = DateTimeUtils.year(start, timeZone());
final int yearEnd = DateTimeUtils.year(end, timeZone());
if (yearStart == yearEnd) {
return (double) diffBusinessNanos(start, end) / (double) getYearData(yearStart).businessTimeNanos;
}
final YearData yearDataStart = getYearData(yearStart);
final YearData yearDataEnd = getYearData(yearEnd);
return (double) diffBusinessNanos(start, yearDataStart.end) / (double) yearDataStart.businessTimeNanos +
(double) diffBusinessNanos(yearDataEnd.start, end) / (double) yearDataEnd.businessTimeNanos +
yearEnd - yearStart - 1;
}
/**
* Returns the number of business years between {@code start} and {@code end}.
*
* @param start start; if null, return null
* @param end end; if null, return null
* @return the amount of business time in business years between the {@code start} and {@code end}.
* {@link QueryConstants#NULL_DOUBLE} if any input is {@code null}.
* @throws InvalidDateException if the dates are not in the valid range
*/
public double diffBusinessYears(final ZonedDateTime start, final ZonedDateTime end) {
if (start == null || end == null) {
return NULL_DOUBLE;
}
return diffBusinessYears(start.toInstant(), end.toInstant());
}
// endregion
// region Arithmetic
/**
* Adds a specified number of business days to an input date. Adding negative days is equivalent to subtracting
* days.
*
* @param date date
* @param days number of days to add.
* @return {@code days} business days after {@code date}. {@code }null} if {@code date} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public LocalDate plusBusinessDays(final LocalDate date, final int days) {
if (date == null || days == NULL_INT) {
return null;
}
if (days == 0) {
return isBusinessDay(date) ? date : null;
}
final int step = days > 0 ? 1 : -1;
LocalDate d = date;
int count = 0;
while (count != days) {
d = d.plusDays(step);
count += isBusinessDay(d) ? step : 0;
}
return d;
}
/**
* Adds a specified number of business days to an input date. Adding negative days is equivalent to subtracting
* days.
*
* @param date date
* @param days number of days to add.
* @return {@code days} business days after {@code date}. {@code null} if {@code date} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public String plusBusinessDays(final String date, final int days) {
if (date == null || days == NULL_INT) {
return null;
}
final LocalDate d = plusBusinessDays(DateTimeUtils.parseLocalDate(date), days);
return d == null ? null : d.toString();
}
/**
* Adds a specified number of business days to an input time. Adding negative days is equivalent to subtracting
* days.
*
* Day additions are not always 24 hours. The resultant time will have the same local time as the input time, as
* determined by the calendar's time zone. This accounts for Daylight Savings Time. For example, 2023-11-05 has a
* daylight savings time adjustment, so '2023-11-04T14:00 ET' plus 1 day will result in '2023-11-05T15:00 ET', which
* is a 25-hour difference.
*
* @param time time
* @param days number of days to add.
* @return {@code days} business days after {@code time}. {@code null} if {@code time} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public Instant plusBusinessDays(final Instant time, final int days) {
if (time == null || days == NULL_INT) {
return null;
}
final ZonedDateTime zdt = plusBusinessDays(DateTimeUtils.toZonedDateTime(time, timeZone()), days);
return zdt == null ? null : zdt.toInstant();
}
/**
* Adds a specified number of business days to an input time. Adding negative days is equivalent to subtracting
* days.
*
* Day additions are not always 24 hours. The resultant time will have the same local time as the input time, as
* determined by the calendar's time zone. This accounts for Daylight Savings Time. For example, 2023-11-05 has a
* daylight savings time adjustment, so '2023-11-04T14:00 ET' plus 1 day will result in '2023-11-05T15:00 ET', which
* is a 25-hour difference.
*
* The resultant time will have the same time zone as the calendar. This could be different than the time zone of
* the input {@link ZonedDateTime}.
*
* @param time time
* @param days number of days to add.
* @return {@code days} business days after {@code time}. {@code null} if {@code time} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public ZonedDateTime plusBusinessDays(final ZonedDateTime time, final int days) {
if (time == null || days == NULL_INT) {
return null;
}
final ZonedDateTime zdt = time.withZoneSameInstant(timeZone());
final LocalDate pbd = plusBusinessDays(zdt.toLocalDate(), days);
return pbd == null ? null
: pbd
.atTime(zdt.toLocalTime())
.atZone(timeZone());
}
/**
* Subtracts a specified number of business days from an input date. Subtracting negative days is equivalent to
* adding days.
*
* @param date date
* @param days number of days to add.
* @return {@code days} business days before {@code date}. {@code null} if {@code date} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public LocalDate minusBusinessDays(final LocalDate date, final int days) {
if (date == null || days == NULL_INT) {
return null;
}
return plusBusinessDays(date, -days);
}
/**
* Subtracts a specified number of business days from an input date. Subtracting negative days is equivalent to
* adding days.
*
* @param date date
* @param days number of days to add.
* @return {@code days} business days before {@code date}. {@code null} if {@code date} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public String minusBusinessDays(final String date, final int days) {
if (date == null || days == NULL_INT) {
return null;
}
return plusBusinessDays(date, -days);
}
/**
* Subtracts a specified number of business days from an input time. Subtracting negative days is equivalent to
* adding days.
*
* Day subtractions are not always 24 hours. The resultant time will have the same local time as the input time, as
* determined by the calendar's time zone. This accounts for Daylight Savings Time. For example, 2023-11-05 has a
* daylight savings time adjustment, so '2023-11-04T14:00 ET' plus 1 day will result in '2023-11-05T15:00 ET', which
* is a 25-hour difference.
*
* @param time time
* @param days number of days to add.
* @return {@code days} business days before {@code time}. {@code null} if {@code time} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public Instant minusBusinessDays(final Instant time, final int days) {
if (time == null || days == NULL_INT) {
return null;
}
return plusBusinessDays(time, -days);
}
/**
* Subtracts a specified number of business days from an input time. Subtracting negative days is equivalent to
* adding days.
*
* Day subtraction are not always 24 hours. The resultant time will have the same local time as the input time, as
* determined by the calendar's time zone. This accounts for Daylight Savings Time. For example, 2023-11-05 has a
* daylight savings time adjustment, so '2023-11-04T14:00 ET' plus 1 day will result in '2023-11-05T15:00 ET', which
* is a 25-hour difference.
*
* The resultant time will have the same time zone as the calendar. This could be different than the time zone of
* the input {@link ZonedDateTime}.
*
* @param time time
* @param days number of days to add.
* @return {@code days} business days before {@code time}. {@code null} if {@code time} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public ZonedDateTime minusBusinessDays(final ZonedDateTime time, final int days) {
if (time == null || days == NULL_INT) {
return null;
}
return plusBusinessDays(time, -days);
}
/**
* Adds a specified number of non-business days to an input date. Adding negative days is equivalent to subtracting
* days.
*
* @param date date
* @param days number of days to add.
* @return {@code days} non-business days after {@code date}. {@code null} if {@code date} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public LocalDate plusNonBusinessDays(final LocalDate date, final int days) {
if (date == null || days == NULL_INT) {
return null;
}
if (days == 0) {
return isBusinessDay(date) ? null : date;
}
final int step = days > 0 ? 1 : -1;
LocalDate d = date;
int count = 0;
while (count != days) {
d = d.plusDays(step);
count += isBusinessDay(d) ? 0 : step;
}
return d;
}
/**
* Adds a specified number of non-business days to an input date. Adding negative days is equivalent to subtracting
* days.
*
* @param date date
* @param days number of days to add.
* @return {@code days} non-business days after {@code date}. {@code null} if {@code date} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public String plusNonBusinessDays(final String date, final int days) {
if (date == null || days == NULL_INT) {
return null;
}
final LocalDate d = this.plusNonBusinessDays(DateTimeUtils.parseLocalDate(date), days);
return d == null ? null : d.toString();
}
/**
* Adds a specified number of non-business days to an input time. Adding negative days is equivalent to subtracting
* days.
*
* Day additions are not always 24 hours. The resultant time will have the same local time as the input time, as
* determined by the calendar's time zone. This accounts for Daylight Savings Time. For example, 2023-11-05 has a
* daylight savings time adjustment, so '2023-11-04T14:00 ET' plus 1 day will result in '2023-11-05T15:00 ET', which
* is a 25-hour difference.
*
* The resultant time will have the same time zone as the calendar. This could be different than the time zone of
* the input {@link ZonedDateTime}.
*
* @param time time
* @param days number of days to add.
* @return {@code days} non-business days after {@code time}. {@code null} if {@code time} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public Instant plusNonBusinessDays(final Instant time, final int days) {
if (time == null || days == NULL_INT) {
return null;
}
final ZonedDateTime zdt = plusNonBusinessDays(DateTimeUtils.toZonedDateTime(time, timeZone()), days);
return zdt == null ? null : zdt.toInstant();
}
/**
* Adds a specified number of non-business days to an input time. Adding negative days is equivalent to subtracting
* days.
*
* Day additions are not always 24 hours. The resultant time will have the same local time as the input time, as
* determined by the calendar's time zone. This accounts for Daylight Savings Time. For example, 2023-11-05 has a
* daylight savings time adjustment, so '2023-11-04T14:00 ET' plus 1 day will result in '2023-11-05T15:00 ET', which
* is a 25-hour difference.
*
* The resultant time will have the same time zone as the calendar. This could be different than the time zone of
* the input {@link ZonedDateTime}.
*
* @param time time
* @param days number of days to add.
* @return {@code days} non-business days after {@code time}. {@code null} if {@code time} is not a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public ZonedDateTime plusNonBusinessDays(final ZonedDateTime time, final int days) {
if (time == null || days == NULL_INT) {
return null;
}
final ZonedDateTime zdt = time.withZoneSameInstant(timeZone());
final LocalDate pbd = plusNonBusinessDays(zdt.toLocalDate(), days);
return pbd == null ? null
: pbd
.atTime(zdt.toLocalTime())
.atZone(timeZone());
}
/**
* Subtracts a specified number of non-business days to an input date. Subtracting negative days is equivalent to
* adding days.
*
* @param date date
* @param days number of days to add.
* @return {@code days} non-business days before {@code date}. {@code null} if {@code date} is a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public LocalDate minusNonBusinessDays(final LocalDate date, final int days) {
if (date == null || days == NULL_INT) {
return null;
}
return this.plusNonBusinessDays(date, -days);
}
/**
* Subtracts a specified number of non-business days to an input date. Subtracting negative days is equivalent to
* adding days.
*
* @param date date
* @param days number of days to add.
* @return {@code days} non-business days before {@code date}. {@code null} if {@code date} is a business day and
* {@code days} is zero.
* @throws InvalidDateException if the date is not in the valid range
* @throws DateTimeUtils.DateTimeParseException if the string cannot be parsed
*/
public String minusNonBusinessDays(final String date, final int days) {
if (date == null || days == NULL_INT) {
return null;
}
return plusNonBusinessDays(date, -days);
}
/**
* Subtracts a specified number of non-business days to an input time. Subtracting negative days is equivalent to
* adding days.
*
* Day subtractions are not always 24 hours. The resultant time will have the same local time as the input time, as
* determined by the calendar's time zone. This accounts for Daylight Savings Time. For example, 2023-11-05 has a
* daylight savings time adjustment, so '2023-11-04T14:00 ET' plus 1 day will result in '2023-11-05T15:00 ET', which
* is a 25-hour difference.
*
* @param time time
* @param days number of days to add.
* @return {@code days} non-business days before {@code time}. {@code null} if {@code time} is a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public Instant minusNonBusinessDays(final Instant time, final int days) {
if (time == null || days == NULL_INT) {
return null;
}
return plusNonBusinessDays(time, -days);
}
/**
* Subtracts a specified number of non-business days to an input time. Subtracting negative days is equivalent to
* adding days.
*
* Day subtractions are not always 24 hours. The resultant time will have the same local time as the input time, as
* determined by the calendar's time zone. This accounts for Daylight Savings Time. For example, 2023-11-05 has a
* daylight savings time adjustment, so '2023-11-04T14:00 ET' plus 1 day will result in '2023-11-05T15:00 ET', which
* is a 25-hour difference.
*
* The resultant time will have the same time zone as the calendar. This could be different than the time zone of
* the input {@link ZonedDateTime}.
*
* @param time time
* @param days number of days to add.
* @return {@code days} non-business days before {@code time}. {@code null} if {@code time} is a business day and
* {@code days} is zero. {@code null} if inputs are {@code null} or {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public ZonedDateTime minusNonBusinessDays(final ZonedDateTime time, final int days) {
if (time == null || days == NULL_INT) {
return null;
}
return plusNonBusinessDays(time, -days);
}
/**
* Adds a specified number of business days to the current date. Adding negative days is equivalent to subtracting
* days.
*
* @param days number of days to add.
* @return {@code days} business days after the current date. {@code null} if the current date is not a business day
* and {@code days} is zero. {@code null} if input is {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public LocalDate futureBusinessDate(final int days) {
if (days == NULL_INT) {
return null;
}
return plusBusinessDays(calendarDate(), days);
}
/**
* Subtracts a specified number of business days from the current date. Subtracting negative days is equivalent to
* adding days.
*
* @param days number of days to subtract.
* @return {@code days} business days before the current date. {@code null} if the current date is not a business
* day and {@code days} is zero. {@code null} if input is {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public LocalDate pastBusinessDate(final int days) {
if (days == NULL_INT) {
return null;
}
return minusBusinessDays(calendarDate(), days);
}
/**
* Adds a specified number of non-business days to the current date. Adding negative days is equivalent to
* subtracting days.
*
* @param days number of days to add.
* @return {@code days} non-business days after the current date. {@code null} if the current date is a business day
* and {@code days} is zero. {@code null} if input is {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public LocalDate futureNonBusinessDate(final int days) {
if (days == NULL_INT) {
return null;
}
return this.plusNonBusinessDays(calendarDate(), days);
}
/**
* Subtracts a specified number of non-business days to the current date. Subtracting negative days is equivalent to
* adding days.
*
* @param days number of days to subtract.
* @return {@code days} non-business days before the current date. {@code null} if the current date is a business
* day and {@code days} is zero. {@code null} if input is {@link QueryConstants#NULL_INT}.
* @throws InvalidDateException if the date is not in the valid range
*/
public LocalDate pastNonBusinessDate(final int days) {
if (days == NULL_INT) {
return null;
}
return minusNonBusinessDays(calendarDate(), days);
}
// endregion
}