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))