io.deephaven.time.calendar.BusinessCalendarXMLParser Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of deephaven-engine-time Show documentation
Show all versions of deephaven-engine-time Show documentation
Engine Time: Types and libraries for working with instants, periods, and calendars
//
// 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.time.TimeZoneAliases;
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.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.time.*;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* A parser for reading business calendar XML files.
*
*
* Business calendar XML files should be formatted as:
*
*
* {@code
*
* USNYSE
*
* New York Stock Exchange Calendar
* America/New_York
*
* 09:30 16:00
* Saturday
* Sunday
*
*
* 1999-01-01
*
* 2003-12-31
*
* 1999-01-01
*
*
* 2002-07-05
*
* 09:30
* 13:00
*
*
*
* }
*
*
* In addition, legacy XML files are supported. These files have dates formatted as `yyyyMMdd` instead of ISO-8601
* `yyy-MM-dd`. Additionally, legacy uses `businessPeriod` tags in place of the `businessTime` tags.
*
*
* {@code
*
* 09:30 16:00
* }
*
*
*
* {@code
*
* 09:30,16:00
* }
*
*
* The legacy format may be deprecated in a future release.
*/
public final class BusinessCalendarXMLParser {
private static class BusinessCalendarInputs {
private String calendarName;
private String description;
private ZoneId timeZone;
private LocalDate firstValidDate;
private LocalDate lastValidDate;
private CalendarDay standardBusinessDay;
private Set weekendDays;
private Map> holidays;
}
/**
* Loads a business calendar from an XML file.
*
* @param file XML file
* @return business calendar.
* @throws RequirementFailure if the input is null
*/
public static BusinessCalendar loadBusinessCalendar(@NotNull final String file) {
Require.neqNull(file, "file");
return loadBusinessCalendar(new File(file));
}
/**
* Loads a business calendar from an XML file.
*
* @param file XML file
* @return business calendar.
* @throws RequirementFailure if the input is null
*/
public static BusinessCalendar loadBusinessCalendar(@NotNull final File file) {
Require.neqNull(file, "file");
final BusinessCalendarInputs in = parseBusinessCalendarInputs(file);
return new BusinessCalendar(in.calendarName, in.description,
in.timeZone, in.firstValidDate, in.lastValidDate,
in.standardBusinessDay, in.weekendDays, in.holidays);
}
/**
* Loads a business calendar from an XML input stream.
*
* @param inputStream XML input stream
* @return business calendar.
* @throws RequirementFailure if the input is null
*/
public static BusinessCalendar loadBusinessCalendar(@NotNull final InputStream inputStream) {
Require.neqNull(inputStream, "inputStream");
final BusinessCalendarInputs in = parseBusinessCalendarInputs(inputStream);
return new BusinessCalendar(in.calendarName, in.description,
in.timeZone, in.firstValidDate, in.lastValidDate,
in.standardBusinessDay, in.weekendDays, in.holidays);
}
/**
* Loads a business calendar from an XML resource.
*
* @param resource XML input stream
* @return business calendar.
*/
public static BusinessCalendar loadBusinessCalendarFromResource(String resource) throws IOException {
final InputStream in = Calendars.class.getResourceAsStream(resource);
if (in == null) {
throw new RuntimeException("Could not open resource " + resource + " from classpath");
}
try (final InputStream bin = new BufferedInputStream(in)) {
return loadBusinessCalendar(bin);
}
}
private static BusinessCalendarInputs parseBusinessCalendarInputs(@NotNull final File file) {
Require.neqNull(file, "file");
try {
return fill(loadXMLRootElement(file));
} catch (Exception e) {
throw new RuntimeException("Unable to load calendar file: file=" + file.getPath(), e);
}
}
private static BusinessCalendarInputs parseBusinessCalendarInputs(@NotNull final InputStream in) {
Require.neqNull(in, "in");
try {
return fill(loadXMLRootElement(in));
} catch (Exception e) {
throw new RuntimeException("Unable to load calendar file: inputStream=" + in, e);
}
}
private static BusinessCalendarInputs fill(Element root) throws Exception {
final BusinessCalendarInputs calendarElements = new BusinessCalendarInputs();
calendarElements.calendarName = getText(getRequiredChild(root, "name"));
calendarElements.timeZone = TimeZoneAliases.zoneId(getText(getRequiredChild(root, "timeZone")));
calendarElements.description = getText(root.getChild("description"));
calendarElements.holidays = parseHolidays(root, calendarElements.timeZone);
final String firstValidDateStr = getText(root.getChild("firstValidDate"));
calendarElements.firstValidDate =
firstValidDateStr == null ? Collections.min(calendarElements.holidays.keySet())
: DateTimeUtils.parseLocalDate(firstValidDateStr);
final String lastValidDateStr = getText(root.getChild("lastValidDate"));
calendarElements.lastValidDate =
lastValidDateStr == null ? Collections.max(calendarElements.holidays.keySet())
: DateTimeUtils.parseLocalDate(lastValidDateStr);
// Set the default values
final Element defaultElement = getRequiredChild(root, "default");
calendarElements.weekendDays = parseWeekendDays(defaultElement);
calendarElements.standardBusinessDay = parseCalendarDaySchedule(defaultElement);
return calendarElements;
}
private static Element loadXMLRootElement(File calendarFile) throws Exception {
final Document doc;
try {
final SAXBuilder builder = new SAXBuilder();
doc = builder.build(calendarFile);
} catch (JDOMException e) {
throw new Exception("Error parsing business calendar: file=" + calendarFile, e);
} catch (IOException e) {
throw new Exception("Error loading business calendar: file=" + calendarFile, e);
}
return doc.getRootElement();
}
private static Element loadXMLRootElement(InputStream in) throws Exception {
final Document doc;
try {
final SAXBuilder builder = new SAXBuilder();
doc = builder.build(in);
} catch (JDOMException e) {
throw new Exception("Error parsing business calendar: inputStream=" + in, e);
} catch (IOException e) {
throw new Exception("Error loading business calendar: inputStream=" + in, e);
}
return doc.getRootElement();
}
private static Element getRequiredChild(@NotNull final Element root, final String child) throws Exception {
Element element = root.getChild(child);
if (element != null) {
return element;
} else {
throw new Exception("Missing the '" + child + "' tag in calendar file: text=" + root.getTextTrim());
}
}
private static String getText(Element element) {
return element == null ? null : element.getTextTrim();
}
private static CalendarDay parseCalendarDaySchedule(final Element element) throws Exception {
final List businessTimes = element.getChildren("businessTime");
final List businessPeriods = element.getChildren("businessPeriod");
if (businessTimes.isEmpty() && businessPeriods.isEmpty()) {
return CalendarDay.HOLIDAY;
} else if (!businessTimes.isEmpty() && businessPeriods.isEmpty()) {
return new CalendarDay<>(parseBusinessRanges(businessTimes));
} else if (businessTimes.isEmpty() && !businessPeriods.isEmpty()) {
return new CalendarDay<>(parseBusinessRangesLegacy(businessPeriods));
} else {
throw new Exception("Cannot have both 'businessTime' and 'businessPeriod' tags in the same element: text="
+ element.getTextTrim());
}
}
private static TimeRange[] parseBusinessRanges(final List businessRanges)
throws Exception {
// noinspection unchecked
final TimeRange[] rst = new TimeRange[businessRanges.size()];
for (int i = 0; i < businessRanges.size(); i++) {
final String openTxt = getText(getRequiredChild(businessRanges.get(i), "open"));
final String closeTxt = getText(getRequiredChild(businessRanges.get(i), "close"));
if (closeTxt.startsWith("24:00")) {
throw new RuntimeException("Close time (" + closeTxt
+ ") is on the next day. '23:59:59.999999999' is the maximum close time.");
}
final LocalTime open = DateTimeUtils.parseLocalTime(openTxt);
final LocalTime close = DateTimeUtils.parseLocalTime(closeTxt);
rst[i] = new TimeRange<>(open, close, true);
}
return rst;
}
private static TimeRange[] parseBusinessRangesLegacy(final List businessRanges)
throws Exception {
// noinspection unchecked
final TimeRange[] rst = new TimeRange[businessRanges.size()];
int i = 0;
for (Element br : businessRanges) {
final String[] openClose = br.getTextTrim().split(",");
if (openClose.length == 2) {
final String openTxt = openClose[0];
final String closeTxt = openClose[1];
final LocalTime open = DateTimeUtils.parseLocalTime(openTxt);
final LocalTime close = DateTimeUtils.parseLocalTime(closeTxt);
rst[i] = new TimeRange<>(open, close, true);
} else {
throw new IllegalArgumentException("Can not parse business periods; open/close = " + br.getText());
}
i++;
}
return rst;
}
private static Map> parseHolidays(final Element root, final ZoneId timeZone)
throws Exception {
final Map> holidays = new ConcurrentHashMap<>();
final List holidayElements = root.getChildren("holiday");
for (Element holidayElement : holidayElements) {
final Element dateElement = getRequiredChild(holidayElement, "date");
String dateStr = getText(dateElement);
// Convert yyyyMMdd to yyyy-MM-dd
if (dateStr.length() == 8) {
dateStr = dateStr.substring(0, 4) + "-" + dateStr.substring(4, 6) + "-" + dateStr.substring(6, 8);
}
final LocalDate date = DateTimeUtils.parseLocalDate(dateStr);
final CalendarDay schedule = parseCalendarDaySchedule(holidayElement);
holidays.put(date, CalendarDay.toInstant(schedule, date, timeZone));
}
return holidays;
}
private static Set parseWeekendDays(@NotNull final Element defaultElement) throws Exception {
final Set weekendDays = new HashSet<>();
final List weekends = defaultElement.getChildren("weekend");
if (weekends != null) {
for (Element weekendElement : weekends) {
final String weekend = getText(weekendElement);
final String dows = weekend.trim().toUpperCase();
final DayOfWeek dow;
try {
dow = DayOfWeek.valueOf(dows);
} catch (IllegalArgumentException e) {
throw new Exception("Invalid day of week: day=" + dows, e);
}
weekendDays.add(dow);
}
}
return weekendDays;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy