Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.deephaven.time.calendar.DefaultBusinessCalendar Maven / Gradle / Ivy
Go to download
Engine Time: Types and libraries for working with instants, periods, and calendars
/**
* Copyright (c) 2016-2022 Deephaven Data Labs and Patent Pending
*/
package io.deephaven.time.calendar;
import io.deephaven.base.Pair;
import io.deephaven.time.DateTimeUtils;
import io.deephaven.time.TimeZoneAliases;
import io.deephaven.util.QueryConstants;
import io.deephaven.util.annotations.VisibleForTesting;
import org.jdom2.Document;
import org.jdom2.Element;
import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.Calendar;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
/**
* Default implementation for a {@link BusinessCalendar}. This implementation is thread safe.
*
* Overrides many default {@link Calendar} and BusinessCalendar methods for improved performance. See the documentation
* of Calendar for details.
*/
public class DefaultBusinessCalendar extends AbstractBusinessCalendar implements Serializable {
private static final long serialVersionUID = -5887343387358189382L;
// our "null" holder for holidays
private static final BusinessSchedule HOLIDAY = new Holiday();
private static final DateTimeFormatter HOLIDAY_PARSER =
DateTimeFormatter.ofPattern("yyyyMMdd").withLocale(new Locale("en", "US"));
// each calendar has a name, timezone, and date string format
private final String calendarName;
private final ZoneId timeZone;
// length, in nanos, that a default day is open
private final long lengthOfDefaultDayNanos;
// holds the open hours for a standard business day
private final List defaultBusinessPeriodStrings;
private final Set weekendDays;
private final Map dates;
private final Map holidays;
private final Map cachedYearLengths = new ConcurrentHashMap<>();
@VisibleForTesting
DefaultBusinessCalendar(final CalendarElements calendarElements) {
this(calendarElements.calendarName, calendarElements.timeZone, calendarElements.lengthOfDefaultDayNanos,
calendarElements.defaultBusinessPeriodStrings, calendarElements.weekendDays, calendarElements.dates,
calendarElements.holidays);
}
private DefaultBusinessCalendar(final String calendarName, final ZoneId timeZone,
final long lengthOfDefaultDayNanos, final List defaultBusinessPeriodStrings,
final Set weekendDays, final Map dates,
final Map holidays) {
this.calendarName = calendarName;
this.timeZone = timeZone;
this.lengthOfDefaultDayNanos = lengthOfDefaultDayNanos;
this.defaultBusinessPeriodStrings = defaultBusinessPeriodStrings;
this.weekendDays = weekendDays;
this.dates = dates;
this.holidays = holidays;
}
static BusinessCalendar getInstance(@NotNull final File calendarFile) {
final CalendarElements calendarElements = constructCalendarElements(calendarFile);
final BusinessCalendar calendar = new DefaultBusinessCalendar(calendarElements);
if (!calendarElements.hasHolidays && calendarElements.weekendDays.isEmpty()) {
return new DefaultNoHolidayBusinessCalendar(calendar);
}
return calendar;
}
@VisibleForTesting
static CalendarElements constructCalendarElements(@NotNull final File calendarFile) {
final CalendarElements calendarElements = new CalendarElements();
final String filePath = calendarFile.getPath();
Element root = getRootElement(calendarFile);
calendarElements.calendarName = getCalendarName(root, filePath);
calendarElements.timeZone = getTimeZone(root, filePath);
// Set the default values
final Element defaultElement = getRequiredChild(root, "default", filePath);
calendarElements.defaultBusinessPeriodStrings = getDefaultBusinessPeriodStrings(defaultElement, filePath);
calendarElements.weekendDays = getWeekendDays(defaultElement);
// get the holidays/half days
final Pair, Map> datePair =
getDates(root, calendarElements);
calendarElements.dates = datePair.getFirst();
calendarElements.holidays = datePair.getSecond();
calendarElements.lengthOfDefaultDayNanos =
parseStandardBusinessDayLengthNanos(calendarElements.defaultBusinessPeriodStrings);
return calendarElements;
}
private static Pair, Map> getDates(final Element root,
final CalendarElements calendarElements) {
final Map holidays = new ConcurrentHashMap<>();
// initialize the calendar to the years placed in the calendar file
int minYear = Integer.MAX_VALUE;
int maxYear = Integer.MIN_VALUE;
final List holidayElements = root.getChildren("holiday");
calendarElements.hasHolidays = !holidayElements.isEmpty();
for (Element holidayElement : holidayElements) {
final Element date = holidayElement.getChild("date");
if (date != null) {
final String dateStr = getText(date);
final LocalDate localDate = parseLocalDate(dateStr);
final int holidayYear = localDate.getYear();
if (holidayYear < minYear) {
minYear = holidayYear;
}
if (holidayYear > maxYear) {
maxYear = holidayYear;
}
final List businessPeriodsList = holidayElement.getChildren("businessPeriod");
final List businessPeriodStrings = new ArrayList<>();
for (Element busPeriod : businessPeriodsList) {
String businessPeriod = getText(busPeriod);
businessPeriodStrings.add(businessPeriod.trim());
}
if (businessPeriodStrings.isEmpty()) {
holidays.put(localDate, HOLIDAY);
} else {
holidays.put(localDate, new BusinessSchedule(
parseBusinessPeriods(calendarElements.timeZone, localDate, businessPeriodStrings)));
}
}
}
final Map dates = new ConcurrentHashMap<>(holidays);
if (minYear != Integer.MAX_VALUE && maxYear != Integer.MIN_VALUE) {
for (int i = 0; i < maxYear - minYear; i++) {
int year = i + minYear;
boolean isLeap = DateStringUtils.isLeapYear(year);
int numDays = 365 + (isLeap ? 1 : 0);
for (int j = 0; j < numDays; j++) {
dates.computeIfAbsent(LocalDate.ofYearDay(year, j + 1),
date -> newBusinessDay(date, calendarElements.weekendDays, calendarElements.timeZone,
calendarElements.defaultBusinessPeriodStrings));
}
}
}
return new Pair<>(dates, holidays);
}
private static Set getWeekendDays(@NotNull final Element defaultElement) {
final Set weekendDays = new HashSet<>();
final List weekends = defaultElement.getChildren("weekend");
if (weekends != null) {
for (Element weekendElement : weekends) {
String weekend = getText(weekendElement);
weekendDays.add(DayOfWeek.valueOf(weekend.trim().toUpperCase()));
}
}
return weekendDays;
}
private static List getDefaultBusinessPeriodStrings(@NotNull final Element defaultElement,
final String filePath) {
final List defaultBusinessPeriodStrings = new ArrayList<>();
final List businessPeriods = defaultElement.getChildren("businessPeriod");
if (businessPeriods != null) {
for (Element businessPeriod1 : businessPeriods) {
String businessPeriod = getText(businessPeriod1);
defaultBusinessPeriodStrings.add(businessPeriod);
}
} else {
throw new IllegalArgumentException(
"Missing the 'businessPeriod' tag in the 'default' section in calendar file " + filePath);
}
return defaultBusinessPeriodStrings;
}
private static Element getRootElement(File calendarFile) {
final Document doc;
try {
final SAXBuilder builder = new SAXBuilder();
doc = builder.build(calendarFile);
} catch (JDOMException e) {
throw new IllegalArgumentException(
"Could not initialize business calendar: Error parsing " + calendarFile.getName());
} catch (IOException e) {
throw new RuntimeException(
"Could not initialize business calendar: " + calendarFile.getName() + " could not be loaded");
}
return doc.getRootElement();
}
private static String getCalendarName(@NotNull final Element root, final String filePath) {
final Element element = getRequiredChild(root, "name", filePath);
return getText(element);
}
private static ZoneId getTimeZone(@NotNull final Element root, final String filePath) {
final Element element = getRequiredChild(root, "timeZone", filePath);
return TimeZoneAliases.zoneId(getText(element));
}
// throws an error if the child is missing
private static Element getRequiredChild(@NotNull final Element root, final String child, final String filePath) {
Element element = root.getChild(child);
if (element != null) {
return element;
} else {
throw new IllegalArgumentException("Missing the " + child + " tag in calendar file " + filePath);
}
}
private static LocalDate parseLocalDate(final String date) {
try {
return DateStringUtils.parseLocalDate(date);
} catch (Exception e) {
try {
return LocalDate.parse(date, HOLIDAY_PARSER);
} catch (Exception ee) {
throw new IllegalArgumentException(
"Malformed date string. Acceptable formats are yyyy-MM-dd and yyyyMMdd. s=" + date);
}
}
}
private static long parseStandardBusinessDayLengthNanos(final List defaultBusinessPeriodStrings) {
long lengthOfDefaultDayNanos = 0;
Pattern hhmm = Pattern.compile("\\d{2}:\\d{2}");
for (String businessPeriodString : defaultBusinessPeriodStrings) {
String[] openClose = businessPeriodString.split(",");
boolean wellFormed = false;
if (openClose.length == 2) {
String open = openClose[0];
String close = openClose[1];
if (hhmm.matcher(open).matches() && hhmm.matcher(close).matches()) {
String[] openingTimeHHMM = open.split(":");
String[] closingTimeHHMM = close.split(":");
long defOpenTimeNanos = (Integer.parseInt(openingTimeHHMM[0]) * DateTimeUtils.HOUR)
+ (Integer.parseInt(openingTimeHHMM[1]) * DateTimeUtils.MINUTE);
long defClosingTimeNanos = (Integer.parseInt(closingTimeHHMM[0]) * DateTimeUtils.HOUR)
+ (Integer.parseInt(closingTimeHHMM[1]) * DateTimeUtils.MINUTE);
lengthOfDefaultDayNanos += defClosingTimeNanos - defOpenTimeNanos;
wellFormed = true;
}
}
if (!wellFormed) {
throw new UnsupportedOperationException("Could not parse business period " + businessPeriodString);
}
}
return lengthOfDefaultDayNanos;
}
private static BusinessPeriod[] parseBusinessPeriods(final ZoneId timeZone, final LocalDate date,
final List businessPeriodStrings) {
final BusinessPeriod[] businessPeriods = new BusinessPeriod[businessPeriodStrings.size()];
final Pattern hhmm = Pattern.compile("\\d{2}[:]\\d{2}");
int i = 0;
for (String businessPeriodString : businessPeriodStrings) {
final String[] openClose = businessPeriodString.split(",");
if (openClose.length == 2) {
final String open = openClose[0];
String close = openClose[1];
if (hhmm.matcher(open).matches() && hhmm.matcher(close).matches()) {
final String tz = TimeZoneAliases.zoneName(timeZone);
final LocalDate closeDate;
if (close.equals("24:00")) { // midnight closing time
closeDate = date.plusDays(1);
close = "00:00";
} else if (Integer.parseInt(open.replaceAll(":", "")) > Integer
.parseInt(close.replaceAll(":", ""))) {
throw new IllegalArgumentException(
"Can not parse business periods; open = " + open + " is greater than close = " + close);
} else {
closeDate = date;
}
final String openDateStr = date.toString() + "T" + open + " " + tz;
final String closeDateStr = closeDate.toString() + "T" + close + " " + tz;
businessPeriods[i++] = new BusinessPeriod(DateTimeUtils.parseInstant(openDateStr),
DateTimeUtils.parseInstant(closeDateStr));
}
}
}
return businessPeriods;
}
@Override
public List getDefaultBusinessPeriods() {
return Collections.unmodifiableList(defaultBusinessPeriodStrings);
}
@Override
public Map getHolidays() {
return Collections.unmodifiableMap(holidays);
}
@Override
public boolean isBusinessDay(DayOfWeek day) {
return !weekendDays.contains(day);
}
@Override
public String name() {
return calendarName;
}
@Override
public ZoneId timeZone() {
return timeZone;
}
@Override
public long standardBusinessDayLengthNanos() {
return lengthOfDefaultDayNanos;
}
@Override
@Deprecated
public BusinessSchedule getBusinessDay(final Instant time) {
if (time == null) {
return null;
}
final LocalDate localDate = LocalDate.ofYearDay(DateTimeUtils.year(time, timeZone()),
DateTimeUtils.dayOfYear(time, timeZone()));
return getBusinessSchedule(localDate);
}
@Override
@Deprecated
public BusinessSchedule getBusinessDay(final String date) {
if (date == null) {
return null;
}
return getBusinessSchedule(DateStringUtils.parseLocalDate(date));
}
@Override
@Deprecated
public BusinessSchedule getBusinessDay(final LocalDate date) {
return dates.computeIfAbsent(date, this::newBusinessDay);
}
@Override
public BusinessSchedule getBusinessSchedule(final Instant time) {
if (time == null) {
return null;
}
final LocalDate localDate = LocalDate.ofYearDay(DateTimeUtils.year(time, timeZone()),
DateTimeUtils.dayOfYear(time, timeZone()));
return getBusinessSchedule(localDate);
}
@Override
public BusinessSchedule getBusinessSchedule(final String date) {
if (date == null) {
return null;
}
return getBusinessSchedule(DateStringUtils.parseLocalDate(date));
}
@Override
public BusinessSchedule getBusinessSchedule(final LocalDate date) {
return dates.computeIfAbsent(date, this::newBusinessDay);
}
private static String getText(Element element) {
return element == null ? null : element.getTextTrim();
}
private BusinessSchedule newBusinessDay(final LocalDate date) {
return newBusinessDay(date, weekendDays, timeZone(), defaultBusinessPeriodStrings);
}
private static BusinessSchedule newBusinessDay(final LocalDate date, final Set weekendDays,
final ZoneId timeZone, final List businessPeriodStrings) {
if (date == null) {
return null;
}
// weekend
final DayOfWeek dayOfWeek = date.getDayOfWeek();
if (weekendDays.contains(dayOfWeek)) {
return HOLIDAY;
}
return new BusinessSchedule(parseBusinessPeriods(timeZone, date, businessPeriodStrings));
}
@Override
public long diffBusinessNanos(final Instant start, final Instant end) {
if (start == null || end == null) {
return QueryConstants.NULL_LONG;
}
if (DateTimeUtils.isAfter(start, end)) {
return -diffBusinessNanos(end, start);
}
long dayDiffNanos = 0;
Instant day = start;
while (!DateTimeUtils.isAfter(day, end)) {
if (isBusinessDay(day)) {
BusinessSchedule businessDate = getBusinessSchedule(day);
if (businessDate != null) {
for (BusinessPeriod businessPeriod : businessDate.getBusinessPeriods()) {
Instant endOfPeriod = businessPeriod.getEndTime();
Instant startOfPeriod = businessPeriod.getStartTime();
// noinspection StatementWithEmptyBody
if (DateTimeUtils.isAfter(day, endOfPeriod) || DateTimeUtils.isBefore(end, startOfPeriod)) {
// continue
} else if (!DateTimeUtils.isAfter(day, startOfPeriod)) {
if (DateTimeUtils.isBefore(end, endOfPeriod)) {
dayDiffNanos += DateTimeUtils.minus(end, startOfPeriod);
} else {
dayDiffNanos += businessPeriod.getLength();
}
} else {
if (DateTimeUtils.isAfter(end, endOfPeriod)) {
dayDiffNanos += DateTimeUtils.minus(endOfPeriod, day);
} else {
dayDiffNanos += DateTimeUtils.minus(end, day);
}
}
}
}
}
day = getBusinessSchedule(nextBusinessDay(day)).getSOBD();
}
return dayDiffNanos;
}
@Override
public double diffBusinessYear(final Instant startTime, final Instant endTime) {
if (startTime == null || endTime == null) {
return QueryConstants.NULL_DOUBLE;
}
double businessYearDiff = 0.0;
Instant time = startTime;
while (!DateTimeUtils.isAfter(time, endTime)) {
// get length of the business year
final int startYear = DateTimeUtils.year(startTime, timeZone());
final long businessYearLength = cachedYearLengths.computeIfAbsent(startYear, this::getBusinessYearLength);
final Instant endOfYear = getFirstBusinessDateTimeOfYear(startYear + 1);
final long yearDiff;
if (DateTimeUtils.isAfter(endOfYear, endTime)) {
yearDiff = diffBusinessNanos(time, endTime);
} else {
yearDiff = diffBusinessNanos(time, endOfYear);
}
businessYearDiff += (double) yearDiff / (double) businessYearLength;
time = endOfYear;
}
return businessYearDiff;
}
private long getBusinessYearLength(final int year) {
int numDays = 365 + (DateStringUtils.isLeapYear(year) ? 1 : 0);
long yearLength = 0;
for (int j = 0; j < numDays; j++) {
final int day = j + 1;
final BusinessSchedule businessDate = getBusinessSchedule(LocalDate.ofYearDay(year, day));
yearLength += businessDate.getLOBD();
}
return yearLength;
}
private Instant getFirstBusinessDateTimeOfYear(final int year) {
boolean isLeap = DateStringUtils.isLeapYear(year);
int numDays = 365 + (isLeap ? 1 : 0);
for (int j = 0; j < numDays; j++) {
final BusinessSchedule businessDate = getBusinessSchedule(LocalDate.ofYearDay(year, j + 1));
if (!(businessDate instanceof Holiday)) {
return businessDate.getSOBD();
}
}
return null;
}
@Override
public String toString() {
return "DefaultBusinessCalendar{" +
"name='" + calendarName + '\'' +
", timeZone=" + timeZone +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
DefaultBusinessCalendar that = (DefaultBusinessCalendar) o;
return lengthOfDefaultDayNanos == that.lengthOfDefaultDayNanos &&
Objects.equals(calendarName, that.calendarName) &&
timeZone == that.timeZone &&
Objects.equals(defaultBusinessPeriodStrings, that.defaultBusinessPeriodStrings) &&
Objects.equals(weekendDays, that.weekendDays) &&
Objects.equals(holidays, that.holidays);
}
@Override
public int hashCode() {
return Objects.hash(calendarName, timeZone, lengthOfDefaultDayNanos, defaultBusinessPeriodStrings, weekendDays,
holidays);
}
static class CalendarElements {
private String calendarName;
private ZoneId timeZone;
private long lengthOfDefaultDayNanos;
private List defaultBusinessPeriodStrings;
private Set weekendDays;
private Map dates;
private Map holidays;
private boolean hasHolidays;
}
private static class Holiday extends BusinessSchedule implements Serializable {
private static final long serialVersionUID = 5226852380875996172L;
@Override
public BusinessPeriod[] getBusinessPeriods() {
return new BusinessPeriod[0];
}
@Override
public Instant getSOBD() {
throw new UnsupportedOperationException("This is a holiday");
}
@Override
public Instant getEOBD() {
throw new UnsupportedOperationException("This is a holiday");
}
@Override
public long getLOBD() {
return 0;
}
@Override
public boolean isBusinessDay() {
return false;
}
}
}