Skip to content

Commit

Permalink
Parse legacy calendar XML files. (deephaven#5036)
Browse files Browse the repository at this point in the history
  • Loading branch information
chipkent committed Jan 16, 2024
1 parent facc603 commit 4be8b65
Show file tree
Hide file tree
Showing 4 changed files with 612 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@
import java.io.File;
import java.io.IOException;
import java.time.*;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

/**
Expand All @@ -31,20 +28,23 @@
* {@code
* <calendar>
* <name>USNYSE</name>
* <!-- Optional description -->
* <description>New York Stock Exchange Calendar</description>
* <timeZone>America/New_York</timeZone>
* <default>
* <businessTime><open>09:30</open><close>16:00</close></businessTime>
* <weekend>Saturday</weekend>
* <weekend>Sunday</weekend>
* </default>
* <!-- Optional firstValidDate. Defaults to the first holiday. -->
* <firstValidDate>1999-01-01</firstValidDate>
* <!-- Optional lastValidDate. Defaults to the first holiday. -->
* <lastValidDate>2003-12-31</lastValidDate>
* <holiday>
* <date>19990101</date>
* <date>1999-01-01</date>
* </holiday>
* <holiday>
* <date>20020705</date>
* <date>2002-07-05</date>
* <businessTime>
* <open>09:30</open>
* <close>13:00</close>
Expand All @@ -53,6 +53,25 @@
* </calendar>
* }
* </pre>
*
* 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.
*
* <pre>
* {@code
* <!-- Current format -->
* <businessTime><open>09:30</open><close>16:00</close></businessTime>
* }
* </pre>
*
* <pre>
* {@code
* <!-- Legacy format -->
* <businessPeriod>09:30,16:00</businessPeriod>
* }
* </pre>
*
* The legacy format may be deprecated in a future release.
*/
class BusinessCalendarXMLParser {

Expand Down Expand Up @@ -103,12 +122,16 @@ private static BusinessCalendarInputs parseBusinessCalendarInputs(@NotNull final
Element root = loadXMLRootElement(file);
calendarElements.calendarName = getText(getRequiredChild(root, "name"));
calendarElements.timeZone = TimeZoneAliases.zoneId(getText(getRequiredChild(root, "timeZone")));
calendarElements.description = getText(getRequiredChild(root, "description"));
calendarElements.description = getText(root.getChild("description"));
calendarElements.holidays = parseHolidays(root, calendarElements.timeZone);
final String firstValidDateStr = getText(root.getChild("firstValidDate"));
calendarElements.firstValidDate =
DateTimeUtils.parseLocalDate(getText(getRequiredChild(root, "firstValidDate")));
firstValidDateStr == null ? Collections.min(calendarElements.holidays.keySet())
: DateTimeUtils.parseLocalDate(firstValidDateStr);
final String lastValidDateStr = getText(root.getChild("lastValidDate"));
calendarElements.lastValidDate =
DateTimeUtils.parseLocalDate(getText(getRequiredChild(root, "lastValidDate")));
calendarElements.holidays = parseHolidays(root, calendarElements.timeZone);
lastValidDateStr == null ? Collections.max(calendarElements.holidays.keySet())
: DateTimeUtils.parseLocalDate(lastValidDateStr);

// Set the default values
final Element defaultElement = getRequiredChild(root, "default");
Expand Down Expand Up @@ -150,9 +173,19 @@ private static String getText(Element element) {
}

private static CalendarDay<LocalTime> parseCalendarDaySchedule(final Element element) throws Exception {
final List<Element> businessPeriods = element.getChildren("businessTime");
return businessPeriods.isEmpty() ? CalendarDay.HOLIDAY
: new CalendarDay<>(parseBusinessRanges(businessPeriods));
final List<Element> businessTimes = element.getChildren("businessTime");
final List<Element> 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<LocalTime>[] parseBusinessRanges(final List<Element> businessRanges)
Expand All @@ -177,14 +210,46 @@ private static TimeRange<LocalTime>[] parseBusinessRanges(final List<Element> bu
return rst;
}

private static TimeRange<LocalTime>[] parseBusinessRangesLegacy(final List<Element> businessRanges)
throws Exception {
// noinspection unchecked
final TimeRange<LocalTime>[] 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<LocalDate, CalendarDay<Instant>> parseHolidays(final Element root, final ZoneId timeZone)
throws Exception {
final Map<LocalDate, CalendarDay<Instant>> holidays = new ConcurrentHashMap<>();
final List<Element> holidayElements = root.getChildren("holiday");

for (Element holidayElement : holidayElements) {
final Element dateElement = getRequiredChild(holidayElement, "date");
final LocalDate date = DateTimeUtils.parseLocalDate(getText(dateElement));
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<LocalTime> schedule = parseCalendarDaySchedule(holidayElement);
holidays.put(date, CalendarDay.toInstant(schedule, date, timeZone));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ public class Calendar {
* @param name calendar name.
* @param description calendar description.
* @param timeZone calendar time zone.
* @throws RequirementFailure if any parameter is {@code null}
* @throws RequirementFailure if {@code name} or {@code timeZone} is {@code null}
*/
Calendar(final String name, final String description, final ZoneId timeZone) {
this.name = Require.neqNull(name, "name");
this.description = Require.neqNull(description, "description");
this.description = description;
this.timeZone = Require.neqNull(timeZone, "timeZone");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.net.URISyntaxException;
import java.nio.file.Paths;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.Objects;
Expand Down Expand Up @@ -43,4 +44,40 @@ public void testLoad() throws URISyntaxException {
final BusinessCalendar cal = BusinessCalendarXMLParser.loadBusinessCalendar(f);
assertParserTestCal(cal);
}

public static void assertLegacyCal(final BusinessCalendar cal) {
assertEquals("JPOSE", cal.name());
assertNull(cal.description());
assertEquals(DateTimeUtils.timeZone("Asia/Tokyo"), cal.timeZone());
assertEquals(LocalDate.of(2006, 1, 2), cal.firstValidDate());
assertEquals(LocalDate.of(2022, 11, 23), cal.lastValidDate());
assertEquals(2, cal.weekendDays().size());
assertEquals(LocalTime.of(9, 0), cal.standardBusinessDay().businessStart());
assertEquals(LocalTime.of(15, 0), cal.standardBusinessDay().businessEnd());
assertEquals(LocalTime.of(9, 0), cal.standardBusinessDay().businessTimeRanges().get(0).start());
assertEquals(LocalTime.of(11, 30), cal.standardBusinessDay().businessTimeRanges().get(0).end());
assertEquals(LocalTime.of(12, 30), cal.standardBusinessDay().businessTimeRanges().get(1).start());
assertEquals(LocalTime.of(15, 0), cal.standardBusinessDay().businessTimeRanges().get(1).end());
assertTrue(cal.weekendDays().contains(DayOfWeek.SATURDAY));
assertTrue(cal.weekendDays().contains(DayOfWeek.SUNDAY));
assertEquals(156, cal.holidays().size());
assertTrue(cal.holidays().containsKey(LocalDate.of(2006, 1, 3)));
assertTrue(cal.holidays().containsKey(LocalDate.of(2007, 12, 23)));

final CalendarDay<Instant> halfDay = cal.calendarDay("2007-12-28");
assertEquals(1, halfDay.businessTimeRanges().size());
assertEquals(DateTimeUtils.parseInstant("2007-12-28T09:00 Asia/Tokyo"), halfDay.businessStart());
assertEquals(DateTimeUtils.parseInstant("2007-12-28T11:30 Asia/Tokyo"), halfDay.businessEnd());
}

public void testLoadLegacy() throws URISyntaxException {
final String path = Paths
.get(Objects.requireNonNull(TestBusinessCalendarXMLParser.class.getResource("/LEGACY.calendar"))
.toURI())
.toString();
final File f = new File(path);
final BusinessCalendar cal = BusinessCalendarXMLParser.loadBusinessCalendar(f);
assertLegacyCal(cal);
}

}
Loading

0 comments on commit 4be8b65

Please sign in to comment.