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 706eedaf3bc..053cdce3c3d 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 @@ -157,7 +157,7 @@ private static TimeRange[] parseBusinessRanges(final List bu DateTimeUtils.parseLocalTime(getText(getRequiredChild(businessRanges.get(i), "open"))); final LocalTime close = DateTimeUtils.parseLocalTime(getText(getRequiredChild(businessRanges.get(i), "close"))); - rst[i] = new TimeRange<>(open, close); + rst[i] = new TimeRange<>(open, close, true); } return rst; diff --git a/engine/time/src/main/java/io/deephaven/time/calendar/CalendarDay.java b/engine/time/src/main/java/io/deephaven/time/calendar/CalendarDay.java index 0ff1b88da7b..0458ab62caf 100644 --- a/engine/time/src/main/java/io/deephaven/time/calendar/CalendarDay.java +++ b/engine/time/src/main/java/io/deephaven/time/calendar/CalendarDay.java @@ -56,8 +56,9 @@ public class CalendarDay & Temporal> { for (int i = 1; i < ranges.length; i++) { final TimeRange p0 = ranges[i - 1]; final TimeRange p1 = ranges[i]; + final int cmp = p1.start().compareTo(p0.end()); - if (p1.start().compareTo(p0.end()) < 0) { + if (cmp < 0 || (cmp == 0 && p0.isInclusiveEnd())) { throw new IllegalArgumentException("Business time ranges overlap."); } } diff --git a/engine/time/src/main/java/io/deephaven/time/calendar/TimeRange.java b/engine/time/src/main/java/io/deephaven/time/calendar/TimeRange.java index 4596bb9f55a..038178384b2 100644 --- a/engine/time/src/main/java/io/deephaven/time/calendar/TimeRange.java +++ b/engine/time/src/main/java/io/deephaven/time/calendar/TimeRange.java @@ -18,16 +18,19 @@ public class TimeRange & Temporal> { private final T start; private final T end; + private final boolean inclusiveEnd; /** * Create a new time range. * * @param startTime start of the time range. * @param endTime end of the time range. + * @param inclusiveEnd is the end time inclusive? */ - TimeRange(final T startTime, final T endTime) { + TimeRange(final T startTime, final T endTime, final boolean inclusiveEnd) { this.start = startTime; this.end = endTime; + this.inclusiveEnd = inclusiveEnd; if (startTime == null || endTime == null) { throw new IllegalArgumentException("Null argument: startTime=" + startTime + " endTime=" + endTime); @@ -63,13 +66,22 @@ public T end() { return end; } + /** + * Is the end time inclusive? + * + * @return is the end time inclusive? + */ + public boolean isInclusiveEnd() { + return inclusiveEnd; + } + /** * Length of the range in nanoseconds. * * @return length of the range in nanoseconds */ public long nanos() { - return start.until(end, ChronoUnit.NANOS); + return start.until(end, ChronoUnit.NANOS) - (inclusiveEnd ? 0 : 1); } /** @@ -88,9 +100,15 @@ public Duration duration() { * @return true if the time is in this range; otherwise, false. */ public boolean contains(final T time) { - return time != null - && start.compareTo(time) <= 0 - && time.compareTo(end) <= 0; + if(inclusiveEnd) { + return time != null + && start.compareTo(time) <= 0 + && time.compareTo(end) <= 0; + }else { + return time != null + && start.compareTo(time) <= 0 + && time.compareTo(end) < 0; + } } @Override @@ -100,12 +118,12 @@ public boolean equals(Object o) { if (!(o instanceof TimeRange)) return false; TimeRange that = (TimeRange) o; - return start.equals(that.start) && end.equals(that.end); + return start.equals(that.start) && end.equals(that.end) && inclusiveEnd == that.inclusiveEnd; } @Override public int hashCode() { - return Objects.hash(start, end); + return Objects.hash(start, end, inclusiveEnd); } @Override @@ -113,6 +131,7 @@ public String toString() { return "TimeRange{" + "start=" + start + ", end=" + end + + ", inclusiveEnd=" + inclusiveEnd + '}'; } @@ -127,7 +146,7 @@ public String toString() { public static TimeRange toInstant(final TimeRange p, final LocalDate date, final ZoneId timeZone) { return new TimeRange<>(DateTimeUtils.toInstant(date, p.start, timeZone), - DateTimeUtils.toInstant(date, p.end, timeZone)); + DateTimeUtils.toInstant(date, p.end, timeZone), p.inclusiveEnd); } } diff --git a/engine/time/src/test/java/io/deephaven/time/calendar/TestBusinessCalendar.java b/engine/time/src/test/java/io/deephaven/time/calendar/TestBusinessCalendar.java index dff737b8182..0ed0470f574 100644 --- a/engine/time/src/test/java/io/deephaven/time/calendar/TestBusinessCalendar.java +++ b/engine/time/src/test/java/io/deephaven/time/calendar/TestBusinessCalendar.java @@ -17,8 +17,8 @@ public class TestBusinessCalendar extends TestCalendar { private final LocalDate firstValidDate = LocalDate.of(2000, 1, 1); private final LocalDate lastValidDate = LocalDate.of(2050, 12, 31); - private final TimeRange period = new TimeRange<>(LocalTime.of(9, 0), LocalTime.of(12, 15)); - private final TimeRange periodHalf = new TimeRange<>(LocalTime.of(9, 0), LocalTime.of(11, 7)); + private final TimeRange period = new TimeRange<>(LocalTime.of(9, 0), LocalTime.of(12, 15), true); + private final TimeRange periodHalf = new TimeRange<>(LocalTime.of(9, 0), LocalTime.of(11, 7), true); private final CalendarDay schedule = new CalendarDay<>(new TimeRange[] {period}); private final Set weekendDays = Set.of(DayOfWeek.WEDNESDAY, DayOfWeek.THURSDAY); private final LocalDate holidayDate1 = LocalDate.of(2023, 7, 4); diff --git a/engine/time/src/test/java/io/deephaven/time/calendar/TestCalendarDay.java b/engine/time/src/test/java/io/deephaven/time/calendar/TestCalendarDay.java index 4a01e6e35ae..a34e342d90f 100644 --- a/engine/time/src/test/java/io/deephaven/time/calendar/TestCalendarDay.java +++ b/engine/time/src/test/java/io/deephaven/time/calendar/TestCalendarDay.java @@ -19,10 +19,10 @@ public class TestCalendarDay extends BaseArrayTestCase { private final Instant open1 = DateTimeUtils.parseInstant("2017-03-11T10:00:00.000000000 NY"); private final Instant close1 = DateTimeUtils.parseInstant("2017-03-11T11:00:00.000000000 NY"); - private final TimeRange period1 = new TimeRange<>(open1, close1); + private final TimeRange period1 = new TimeRange<>(open1, close1, true); private final Instant open2 = DateTimeUtils.parseInstant("2017-03-11T12:00:00.000000000 NY"); private final Instant close2 = DateTimeUtils.parseInstant("2017-03-11T17:00:00.000000000 NY"); - private final TimeRange period2 = new TimeRange<>(open2, close2); + private final TimeRange period2 = new TimeRange<>(open2, close2, true); public void testEmpty() { final CalendarDay empty = new CalendarDay<>(); @@ -130,8 +130,8 @@ public void testPeriodsOverlap() { } public void testToInstant() { - final TimeRange p1 = new TimeRange<>(LocalTime.of(1, 2), LocalTime.of(3, 4)); - final TimeRange p2 = new TimeRange<>(LocalTime.of(5, 6), LocalTime.of(7, 8)); + final TimeRange p1 = new TimeRange<>(LocalTime.of(1, 2), LocalTime.of(3, 4), true); + final TimeRange p2 = new TimeRange<>(LocalTime.of(5, 6), LocalTime.of(7, 8), true); final CalendarDay local = new CalendarDay<>(new TimeRange[] {p1, p2}); final LocalDate date = LocalDate.of(2017, 3, 11); @@ -152,7 +152,7 @@ public void testEqualsHash() { final CalendarDay multi2 = new CalendarDay<>(new TimeRange[] {period1, period2}); final CalendarDay multi3 = new CalendarDay<>(new TimeRange[] {period1, - new TimeRange<>(open2, DateTimeUtils.parseInstant("2017-03-11T17:01:00.000000000 NY"))}); + new TimeRange<>(open2, DateTimeUtils.parseInstant("2017-03-11T17:01:00.000000000 NY"), true)}); assertEquals(multi, multi); assertEquals(multi, multi2); assertNotEquals(multi, multi3); @@ -162,7 +162,7 @@ public void testEqualsHash() { public void testToString() { final CalendarDay multi = new CalendarDay<>(new TimeRange[] {period1, period2}); assertEquals( - "CalendarDay{businessTimeRanges=[TimeRange{start=2017-03-11T15:00:00Z, end=2017-03-11T16:00:00Z}, TimeRange{start=2017-03-11T17:00:00Z, end=2017-03-11T22:00:00Z}]}", + "CalendarDay{businessTimeRanges=[TimeRange{start=2017-03-11T15:00:00Z, end=2017-03-11T16:00:00Z, inclusiveEnd=true}, TimeRange{start=2017-03-11T17:00:00Z, end=2017-03-11T22:00:00Z, inclusiveEnd=true}]}", multi.toString()); } } diff --git a/engine/time/src/test/java/io/deephaven/time/calendar/TestTimeRange.java b/engine/time/src/test/java/io/deephaven/time/calendar/TestTimeRange.java index 4bf2ab6465a..68ea4c255ae 100644 --- a/engine/time/src/test/java/io/deephaven/time/calendar/TestTimeRange.java +++ b/engine/time/src/test/java/io/deephaven/time/calendar/TestTimeRange.java @@ -14,41 +14,42 @@ public class TestTimeRange extends BaseArrayTestCase { - public void testTimeRange() { + public void testTimeRangeInclusive() { final Instant open1 = DateTimeUtils.parseInstant("2017-03-11T10:00:00.000000000 NY"); final Instant close1 = DateTimeUtils.parseInstant("2017-03-11T11:00:00.000000000 NY"); try { - new TimeRange<>(null, close1); + new TimeRange<>(null, close1, true); TestCase.fail("Expected an exception"); } catch (IllegalArgumentException e) { assertTrue(e.getMessage().contains("null")); } try { - new TimeRange<>(close1, null); + new TimeRange<>(close1, null, true); TestCase.fail("Expected an exception"); } catch (IllegalArgumentException e) { assertTrue(e.getMessage().contains("null")); } try { - new TimeRange<>(close1, open1); + new TimeRange<>(close1, open1, true); TestCase.fail("Expected an exception"); } catch (IllegalArgumentException e) { assertTrue(e.getMessage().contains("after")); } try { - new TimeRange<>(open1, open1); + new TimeRange<>(open1, open1, true); TestCase.fail("Expected an exception"); } catch (IllegalArgumentException e) { assertTrue(e.getMessage().contains("same")); } - TimeRange period = new TimeRange<>(open1, close1); + TimeRange period = new TimeRange<>(open1, close1, true); assertEquals(open1, period.start()); assertEquals(close1, period.end()); + assertTrue(period.isInclusiveEnd()); assertEquals(DateTimeUtils.HOUR, period.nanos()); assertEquals(Duration.ofNanos(DateTimeUtils.HOUR), period.duration()); @@ -64,41 +65,131 @@ public void testTimeRange() { .contains(DateTimeUtils.epochNanosToInstant(DateTimeUtils.epochNanos(close1) + DateTimeUtils.MINUTE))); } - public void testToInstant() { + public void testTimeRangeExclusive() { + final Instant open1 = DateTimeUtils.parseInstant("2017-03-11T10:00:00.000000000 NY"); + final Instant close1 = DateTimeUtils.parseInstant("2017-03-11T11:00:00.000000000 NY"); + + try { + new TimeRange<>(null, close1, false); + TestCase.fail("Expected an exception"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("null")); + } + + try { + new TimeRange<>(close1, null, false); + TestCase.fail("Expected an exception"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("null")); + } + + try { + new TimeRange<>(close1, open1, false); + TestCase.fail("Expected an exception"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("after")); + } + + try { + new TimeRange<>(open1, open1, false); + TestCase.fail("Expected an exception"); + } catch (IllegalArgumentException e) { + assertTrue(e.getMessage().contains("same")); + } + + TimeRange period = new TimeRange<>(open1, close1, false); + assertEquals(open1, period.start()); + assertEquals(close1, period.end()); + assertFalse(period.isInclusiveEnd()); + assertEquals(DateTimeUtils.HOUR-1, period.nanos()); + assertEquals(Duration.ofNanos(DateTimeUtils.HOUR-1), period.duration()); + + assertTrue(period.contains(open1)); + assertTrue(period + .contains(DateTimeUtils.epochNanosToInstant(DateTimeUtils.epochNanos(open1) + DateTimeUtils.MINUTE))); + assertFalse(period + .contains(DateTimeUtils.epochNanosToInstant(DateTimeUtils.epochNanos(open1) - DateTimeUtils.MINUTE))); + assertTrue(period.contains(close1.minusNanos(1))); + assertFalse(period.contains(close1)); + assertTrue(period + .contains(DateTimeUtils.epochNanosToInstant(DateTimeUtils.epochNanos(close1) - DateTimeUtils.MINUTE))); + assertFalse(period + .contains(DateTimeUtils.epochNanosToInstant(DateTimeUtils.epochNanos(close1) + DateTimeUtils.MINUTE))); + } + + public void testToInstantInclusive() { final LocalTime start = LocalTime.of(1, 2, 3); final LocalTime end = LocalTime.of(7, 8, 9); - final TimeRange local = new TimeRange<>(start, end); + final TimeRange local = new TimeRange<>(start, end, true); final LocalDate date = LocalDate.of(2017, 3, 11); final ZoneId timeZone = ZoneId.of("America/Los_Angeles"); final Instant targetStart = date.atTime(start).atZone(timeZone).toInstant(); final Instant targetEnd = date.atTime(end).atZone(timeZone).toInstant(); - final TimeRange target = new TimeRange<>(targetStart, targetEnd); + final TimeRange target = new TimeRange<>(targetStart, targetEnd, true); final TimeRange rst = TimeRange.toInstant(local, date, timeZone); assertEquals(target, rst); } - public void testEqualsHash() { + public void testToInstantExclusive() { final LocalTime start = LocalTime.of(1, 2, 3); final LocalTime end = LocalTime.of(7, 8, 9); - final TimeRange p1 = new TimeRange<>(start, end); - final TimeRange p2 = new TimeRange<>(start, end); - final TimeRange p3 = new TimeRange<>(LocalTime.of(0, 1), end); - final TimeRange p4 = new TimeRange<>(start, LocalTime.of(8, 9)); - assertEquals(p1.hashCode(), Objects.hash(start, end, p1.nanos())); + final TimeRange local = new TimeRange<>(start, end, false); + + final LocalDate date = LocalDate.of(2017, 3, 11); + final ZoneId timeZone = ZoneId.of("America/Los_Angeles"); + final Instant targetStart = date.atTime(start).atZone(timeZone).toInstant(); + final Instant targetEnd = date.atTime(end).atZone(timeZone).toInstant(); + + final TimeRange target = new TimeRange<>(targetStart, targetEnd, false); + final TimeRange rst = TimeRange.toInstant(local, date, timeZone); + assertEquals(target, rst); + } + + public void testEqualsHashInclusive() { + final LocalTime start = LocalTime.of(1, 2, 3); + final LocalTime end = LocalTime.of(7, 8, 9); + final TimeRange p1 = new TimeRange<>(start, end, true); + final TimeRange p2 = new TimeRange<>(start, end, true); + final TimeRange p3 = new TimeRange<>(LocalTime.of(0, 1), end, true); + final TimeRange p4 = new TimeRange<>(start, LocalTime.of(8, 9), true); + + assertEquals(p1.hashCode(), Objects.hash(start, end, true)); + assertEquals(p1, p1); + assertEquals(p1, p2); + assertNotEquals(p1, p3); + assertNotEquals(p1, p4); + } + + public void testEqualsHashExclusive() { + final LocalTime start = LocalTime.of(1, 2, 3); + final LocalTime end = LocalTime.of(7, 8, 9); + final TimeRange p1 = new TimeRange<>(start, end, false); + final TimeRange p2 = new TimeRange<>(start, end, false); + final TimeRange p3 = new TimeRange<>(LocalTime.of(0, 1), end, false); + final TimeRange p4 = new TimeRange<>(start, LocalTime.of(8, 9), false); + + assertEquals(p1.hashCode(), Objects.hash(start, end, false)); assertEquals(p1, p1); assertEquals(p1, p2); assertNotEquals(p1, p3); assertNotEquals(p1, p4); } - public void testToString() { + public void testToStringInclusive() { + final LocalTime start = LocalTime.of(1, 2, 3); + final LocalTime end = LocalTime.of(7, 8, 9); + final TimeRange p1 = new TimeRange<>(start, end, true); + assertEquals("TimeRange{start=01:02:03, end=07:08:09, inclusiveEnd=true}", p1.toString()); + } + + public void testToStringExclusive() { final LocalTime start = LocalTime.of(1, 2, 3); final LocalTime end = LocalTime.of(7, 8, 9); - final TimeRange p1 = new TimeRange<>(start, end); - assertEquals("TimeRange{start=01:02:03, end=07:08:09}", p1.toString()); + final TimeRange p1 = new TimeRange<>(start, end, false); + assertEquals("TimeRange{start=01:02:03, end=07:08:09, inclusiveEnd=false}", p1.toString()); } }