diff --git a/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendarXMLParser.java b/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendarXMLParser.java
index 3b975e021c3..3c4ec57bc5f 100644
--- a/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendarXMLParser.java
+++ b/engine/time/src/main/java/io/deephaven/time/calendar/BusinessCalendarXMLParser.java
@@ -18,10 +18,7 @@
import java.io.IOException;
import java.io.InputStream;
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;
/**
@@ -34,6 +31,7 @@
* {@code
*
* USNYSE
+ *
* New York Stock Exchange Calendar
* America/New_York
*
@@ -41,13 +39,15 @@
* Saturday
* Sunday
*
+ *
* 1999-01-01
+ *
* 2003-12-31
*
- * 19990101
+ * 1999-01-01
*
*
- * 20020705
+ * 2002-07-05
*
* 09:30
* 13:00
@@ -56,6 +56,25 @@
*
* }
*
+ *
+ * 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:3016:00
+ * }
+ *
+ *
+ *
+ * {@code
+ *
+ * 09:30,16:00
+ * }
+ *
+ *
+ * The legacy format may be deprecated in a future release.
*/
public final class BusinessCalendarXMLParser {
@@ -150,12 +169,16 @@ 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(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");
@@ -208,9 +231,19 @@ private static String getText(Element element) {
}
private static CalendarDay parseCalendarDaySchedule(final Element element) throws Exception {
- final List businessPeriods = element.getChildren("businessTime");
- return businessPeriods.isEmpty() ? CalendarDay.HOLIDAY
- : new CalendarDay<>(parseBusinessRanges(businessPeriods));
+ 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)
@@ -235,6 +268,31 @@ private static TimeRange[] parseBusinessRanges(final List bu
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<>();
@@ -242,7 +300,14 @@ private static Map> parseHolidays(final Element
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 schedule = parseCalendarDaySchedule(holidayElement);
holidays.put(date, CalendarDay.toInstant(schedule, date, timeZone));
}
diff --git a/engine/time/src/main/java/io/deephaven/time/calendar/Calendar.java b/engine/time/src/main/java/io/deephaven/time/calendar/Calendar.java
index 163dc25ced6..7f73bd99cb0 100644
--- a/engine/time/src/main/java/io/deephaven/time/calendar/Calendar.java
+++ b/engine/time/src/main/java/io/deephaven/time/calendar/Calendar.java
@@ -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");
}
diff --git a/engine/time/src/test/java/io/deephaven/time/calendar/TestBusinessCalendarXMLParser.java b/engine/time/src/test/java/io/deephaven/time/calendar/TestBusinessCalendarXMLParser.java
index 538fdfc885b..ecb2e4ba308 100644
--- a/engine/time/src/test/java/io/deephaven/time/calendar/TestBusinessCalendarXMLParser.java
+++ b/engine/time/src/test/java/io/deephaven/time/calendar/TestBusinessCalendarXMLParser.java
@@ -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;
@@ -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 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);
+ }
+
}
diff --git a/engine/time/src/test/resources/LEGACY.calendar b/engine/time/src/test/resources/LEGACY.calendar
new file mode 100644
index 00000000000..cb8e4ffc5f2
--- /dev/null
+++ b/engine/time/src/test/resources/LEGACY.calendar
@@ -0,0 +1,494 @@
+
+
+
+ JPOSE
+ Asia/Tokyo
+
+ 09:00,11:30
+ 12:30,15:00
+ Saturday
+ Sunday
+
+
+ 20060102
+
+
+ 20060103
+
+
+ 20060104
+ 09:00,11:30
+
+
+ 20060109
+
+
+ 20060211
+
+
+ 20060321
+
+
+ 20060429
+
+
+ 20060503
+
+
+ 20060504
+
+
+ 20060505
+
+
+ 20060717
+
+
+ 20060918
+
+
+ 20060923
+
+
+ 20061009
+
+
+ 20061103
+
+
+ 20061123
+
+
+ 20061223
+
+
+ 20061229
+ 09:00,11:30
+
+
+ 20070102
+
+
+ 20070103
+
+
+ 20070104
+ 09:00,11:30
+
+
+ 20070108
+
+
+ 20070211
+
+
+ 20070321
+
+
+ 20070429
+
+
+ 20070503
+
+
+ 20070504
+
+
+ 20070505
+
+
+ 20070716
+
+
+ 20070917
+
+
+ 20070923
+
+
+ 20071008
+
+
+ 20071103
+
+
+ 20071123
+
+
+ 20071223
+
+
+ 20071228
+ 09:00,11:30
+
+
+ 20070923
+
+
+ 20071008
+
+
+ 20170101
+
+
+ 20170102
+
+
+ 20170103
+
+
+ 20170109
+
+
+ 20170211
+
+
+ 20170320
+
+
+ 20170429
+
+
+ 20170503
+
+
+ 20170504
+
+
+ 20170505
+
+
+ 20170717
+
+
+ 20170811
+
+
+ 20170918
+
+
+ 20170923
+
+
+ 20171009
+
+
+ 20171103
+
+
+ 20171123
+
+
+ 20171223
+
+
+ 20171231
+
+
+ 20180101
+
+
+ 20180102
+
+
+ 20180103
+
+
+ 20180108
+
+
+ 20180211
+
+
+ 20180212
+
+
+ 20180321
+
+
+ 20180429
+
+
+ 20180430
+
+
+ 20180503
+
+
+ 20180504
+
+
+ 20180505
+
+
+ 20180716
+
+
+ 20180811
+
+
+ 20180917
+
+
+ 20180923
+
+
+ 20180924
+
+
+ 20181008
+
+
+ 20181103
+
+
+ 20181123
+
+
+ 20181223
+
+
+ 20181224
+
+
+ 20181231
+
+
+ 20190101
+
+
+ 20190102
+
+
+ 20190103
+
+
+ 20190114
+
+
+ 20190211
+
+
+ 20190321
+
+
+ 20190429
+
+
+ 20190430
+
+
+ 20190501
+
+
+ 20190502
+
+
+ 20190503
+
+
+ 20190504
+
+
+ 20190506
+
+
+ 20190715
+
+
+ 20190812
+
+
+ 20190916
+
+
+ 20190923
+
+
+ 20191014
+
+
+ 20191022
+
+
+ 20191104
+
+
+ 20191123
+
+
+ 20191231
+
+
+ 20200101
+
+
+ 20200102
+
+
+ 20200103
+
+
+ 20200113
+
+
+ 20200211
+
+
+ 20200224
+
+
+ 20200320
+
+
+ 20200429
+
+
+ 20200504
+
+
+ 20200505
+
+
+ 20200506
+
+
+ 20200723
+
+
+ 20200724
+
+
+ 20200810
+
+
+ 20200921
+
+
+ 20200922
+
+
+ 20201103
+
+
+ 20201123
+
+
+ 20201231
+
+
+ 20210101
+
+
+ 20210102
+
+
+ 20210103
+
+
+ 20210111
+
+
+ 20210211
+
+
+ 20210223
+
+
+ 20210322
+
+
+ 20210429
+
+
+ 20210503
+
+
+ 20210504
+
+
+ 20210505
+
+
+ 20210719
+
+
+ 20210811
+
+
+ 20210920
+
+
+ 20210923
+
+
+ 20211011
+
+
+ 20211103
+
+
+ 20211123
+
+
+ 20211231
+
+
+ 20220101
+
+
+ 20220102
+
+
+ 20220103
+
+
+ 20220110
+
+
+ 20220211
+
+
+ 20220223
+
+
+ 20220321
+
+
+ 20220429
+
+
+ 20220503
+
+
+ 20220504
+
+
+ 20220505
+
+
+ 20220718
+
+
+ 20220811
+
+
+ 20220919
+
+
+ 20220923
+
+
+ 20221010
+
+
+ 20221103
+
+
+ 20221123
+
+
+
+
diff --git a/py/server/deephaven/_udf.py b/py/server/deephaven/_udf.py
index 3ec5b93f95a..4eef4bd697b 100644
--- a/py/server/deephaven/_udf.py
+++ b/py/server/deephaven/_udf.py
@@ -232,7 +232,7 @@ def _parse_signature(fn: Callable) -> _ParsedSignature:
return _parse_np_ufunc_signature(fn)
else:
p_sig = _ParsedSignature(fn=fn)
- sig = inspect.signature(fn)
+ sig = inspect.signature(fn, eval_str=True)
for n, p in sig.parameters.items():
p_sig.params.append(_parse_param_annotation(p.annotation))