org.hamcrest
hamcrest-all
diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java
new file mode 100644
index 00000000..bb55524b
--- /dev/null
+++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java
@@ -0,0 +1,141 @@
+package com.exasol.datatype.interval;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class implements the Exasol-proprietary data type INTERVAL DAY(x) TO SECONDS(y). It supports
+ * conversions to and from strings and from milliseconds.
+ *
+ *
+ * In Exasol this data type represents a time difference consisting of the following components:
+ *
+ *
+ * - days
+ * - hours
+ * - minutes
+ * - seconds
+ * - milliseconds (or fraction of seconds)
+ *
+ *
+ * Since milliseconds are the highest resolution, each interval can also be expressed as a total number of milliseconds.
+ * This is also the recommended way to represent the interval values in other systems which do not natively support this
+ * data type.
+ */
+public class IntervalDayToSecond {
+ private static final long MILLIS_PER_SECOND = 1000L;
+ private static final long SECONDS_PER_MINUTE = 60L;
+ private static final long MINUTES_PER_HOUR = 60L;
+ private static final long HOURS_PER_DAY = 24L;
+ private static final long MILLIS_PER_MINUTE = SECONDS_PER_MINUTE * MILLIS_PER_SECOND;
+ private static final long MILLIS_PER_HOUR = MINUTES_PER_HOUR * MILLIS_PER_MINUTE;
+ private static final long MILLIS_PER_DAY = HOURS_PER_DAY * MILLIS_PER_HOUR;
+ private static final int DAYS_MATCHING_GROUP = 1;
+ private static final int HOURS_MATCHING_GROUP = 2;
+ private static final int MINUTES_MATCHING_GROUP = 3;
+ private static final int SECONDS_MATCHING_GROUP = 4;
+ private static final int MILLIS_MATCHING_GROUP = 5;
+ private static final Pattern INTERVAL_PATTERN = Pattern.compile("(?:(\\d{1,9})\\s+)?" // days
+ + "(\\d{1,2})" // hours
+ + ":(\\d{1,2})" // minutes
+ + "(?::(\\d{1,2})" // seconds
+ + "(?:\\.(\\d{1,3}))?)?" // milliseconds
+ );
+ private final long value;
+
+ private IntervalDayToSecond(final long value) {
+ this.value = value;
+ }
+
+ private IntervalDayToSecond(final String text) {
+ final Matcher matcher = INTERVAL_PATTERN.matcher(text);
+ if (matcher.matches()) {
+ this.value = MILLIS_PER_DAY * parseMatchingGroupToLong(matcher, DAYS_MATCHING_GROUP) //
+ + MILLIS_PER_HOUR * parseMatchingGroupToLong(matcher, HOURS_MATCHING_GROUP) //
+ + MILLIS_PER_MINUTE * parseMatchingGroupToLong(matcher, MINUTES_MATCHING_GROUP) //
+ + MILLIS_PER_SECOND * parseMatchingGroupToLong(matcher, SECONDS_MATCHING_GROUP) //
+ + parseMatchingGroupToLong(matcher, MILLIS_MATCHING_GROUP);
+ } else {
+ throw new IllegalArgumentException(
+ "Text \"" + text + "\" cannot be parsed to an INTERVAL. Must match \"" + INTERVAL_PATTERN + "\"");
+ }
+ }
+
+ private long parseMatchingGroupToLong(final Matcher matcher, final int groupNumber) {
+ return (matcher.group(groupNumber) == null) ? 0 : Long.parseLong(matcher.group(groupNumber));
+ }
+
+ @Override
+ public String toString() {
+ return hasDays() //
+ ? String.format("%d %d:%02d:%02d.%03d", getDays(), getHours(), getMinutes(), getSeconds(), getMillis()) //
+ : String.format("%d:%02d:%02d.%03d", getHours(), getMinutes(), getSeconds(), getMillis());
+ }
+
+ private boolean hasDays() {
+ return this.value >= MILLIS_PER_DAY;
+ }
+
+ private long getDays() {
+ return this.value / MILLIS_PER_DAY;
+ }
+
+ private long getHours() {
+ return this.value / MILLIS_PER_HOUR % HOURS_PER_DAY;
+ }
+
+ private long getMinutes() {
+ return this.value / MILLIS_PER_MINUTE % MINUTES_PER_HOUR;
+ }
+
+ private long getSeconds() {
+ return this.value / MILLIS_PER_SECOND % SECONDS_PER_MINUTE;
+ }
+
+ private long getMillis() {
+ return this.value % MILLIS_PER_SECOND;
+ }
+
+ /**
+ * Create an {@link IntervalDayToSecond} from a number of milliseconds
+ *
+ * @param value total length of the interval in milliseconds
+ * @return interval with milliseconds resolution
+ */
+ // [impl->dsn~exasol.converting-int-to-interval-day-to-second~1]
+ public static IntervalDayToSecond ofMillis(final long value) {
+ return new IntervalDayToSecond(value);
+ }
+
+ /**
+ * Parse an {@link IntervalDayToSecond} from a string
+ *
+ *
+ * The accepted format is:
+ *
+ *
+ * [dddddddd ]hh:mm[:ss[.SSS]]
+ *
+ * Where
+ *
+ *
+ * - d
+ * - day, 1-9 digits, optional
+ * - h
+ * - hours, 1-2 digits, mandatory
+ * - m
+ * - minutes, 1-2 digits, mandatory
+ * - s
+ * - seconds, 1-2 digits, optional
+ * - S
+ * - milliseconds, 1-3 digits, optional
+ *
+ *
+ * @param text string representing an interval
+ * @return interval with milliseconds resolution
+ */
+ // [impl->dsn~exasol.parsing-interval-day-to-second-from-strings~1]
+ public static IntervalDayToSecond parse(final String text) {
+ return new IntervalDayToSecond(text);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java
new file mode 100644
index 00000000..613a615a
--- /dev/null
+++ b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java
@@ -0,0 +1,97 @@
+package com.exasol.datatype.interval;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * This class implements the Exasol-proprietary data type INTERVAL YEAR(x) TO MONTH(y). It supports
+ * conversions to and from strings and from a number of months.
+ *
+ *
+ * In Exasol this data type represents a time difference consisting of the following components:
+ *
+ *
+ *
+ * Since months are the highest resolution, each interval can also be expressed as a total number of months. This is
+ * also the recommended way to represent the interval values in other systems which do not natively support this data
+ * type.
+ */
+public class IntervalYearToMonth {
+ private static final long MONTHS_PER_YEAR = 12L;
+ private static final int YEARS_MATCHING_GROUP = 1;
+ private static final int MONTHS_MATCHING_GROUP = 2;
+ private static final Pattern INTERVAL_PATTERN = Pattern.compile("(\\d{1,9})-(\\d{1,2})");
+ private final long value;
+
+ private IntervalYearToMonth(final long value) {
+ this.value = value;
+ }
+
+ private IntervalYearToMonth(final String text) {
+ final Matcher matcher = INTERVAL_PATTERN.matcher(text);
+ if (matcher.matches()) {
+ this.value = MONTHS_PER_YEAR * parseMatchingGroupToLong(matcher, YEARS_MATCHING_GROUP) //
+ + parseMatchingGroupToLong(matcher, MONTHS_MATCHING_GROUP);
+ } else {
+ throw new IllegalArgumentException(
+ "Text \"" + text + "\" cannot be parsed to an INTERVAL. Must match \"" + INTERVAL_PATTERN + "\"");
+ }
+ }
+
+ private long parseMatchingGroupToLong(final Matcher matcher, final int groupNumber) {
+ return Long.parseLong(matcher.group(groupNumber));
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%d-%02d", getYears(), getMonths());
+ }
+
+ private long getYears() {
+ return this.value / MONTHS_PER_YEAR;
+ }
+
+ private long getMonths() {
+ return this.value % MONTHS_PER_YEAR;
+ }
+
+ /**
+ * Create an {@link IntervalDayToSecond} from a number of months
+ *
+ * @param value total length of the interval in months
+ * @return interval with months resolution
+ */
+ // [impl->dsn~exasol.converting-int-to-interval-year-to-month~1]
+ public static IntervalYearToMonth ofMonths(final long value) {
+ return new IntervalYearToMonth(value);
+ }
+
+ /**
+ * Parse an {@link IntervalDayToSecond} from a string
+ *
+ *
+ * The accepted format is:
+ *
+ *
+ * YYYYYYYYY:MM
+ *
+ * Where
+ *
+ *
+ * - Y
+ * - years, 1-9 digits, mandatory
+ * - M
+ * - months, 1-2 digits, mandatory
+ *
+ *
+ * @param text string representing an interval
+ * @return interval with months resolution
+ */
+ // [impl->dsn~exasol.parsing-interval-year-to-month-from-strings~1]
+ public static IntervalYearToMonth parse(final String text) {
+ return new IntervalYearToMonth(text);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java b/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java
new file mode 100644
index 00000000..285b5461
--- /dev/null
+++ b/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java
@@ -0,0 +1,47 @@
+package com.exasol.datatype.interval;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class TestIntervalDayToSecond {
+ // [utest->dsn~exasol.converting-int-to-interval-day-to-second~1]
+ @ParameterizedTest
+ @CsvSource({ //
+ 0L + ", '0:00:00.000'", //
+ 999L + ", '0:00:00.999'", //
+ 59L * 1000 + ", '0:00:59.000'", //
+ 59L * 60 * 1000 + ", '0:59:00.000'", //
+ 23L * 60 * 60 * 1000 + ", '23:00:00.000'", //
+ 999999999L * 24 * 60 * 60 * 1000 + ", '999999999 0:00:00.000'", //
+ 1L * 24 * 60 * 60 * 1000 + 1 * 60 * 60 * 1000 + 1 * 60 * 1000 + 1 * 1000 + 1 + ", '1 1:01:01.001'" //
+ })
+ void testofMillis(final long value, final String expected) {
+ assertThat(IntervalDayToSecond.ofMillis(value).toString(), equalTo(expected));
+ }
+
+ // [utest->dsn~exasol.parsing-interval-day-to-second-from-strings~1]
+ @ParameterizedTest
+ @CsvSource({ "'0:0', '0:00:00.000'", //
+ "'1:2:3', '1:02:03.000'", //
+ "'11:22:33.444', '11:22:33.444'", //
+ "'1 22:33:44.555', '1 22:33:44.555'", //
+ "'999999999 22:33:44', '999999999 22:33:44.000'" //
+ })
+ void testParse(final String text, final String expected) {
+ assertThat(IntervalDayToSecond.parse(text).toString(), equalTo(expected));
+ }
+
+ // [utest->dsn~exasol.parsing-interval-day-to-second-from-strings~1]
+ @ParameterizedTest
+ @ValueSource(strings = { "0", ":0", "1.0", "123:45", "12:234", "12:34:567", "12:34:56:7890", //
+ "1000000000 0:0" //
+ })
+ void testParseIllegalInputThrowsException(final String text) {
+ assertThrows(IllegalArgumentException.class, () -> IntervalDayToSecond.parse(text));
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java
new file mode 100644
index 00000000..8f25d81e
--- /dev/null
+++ b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java
@@ -0,0 +1,42 @@
+package com.exasol.datatype.interval;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class TestIntervalYearToMonth {
+ // [utest->dsn~exasol.converting-int-to-interval-year-to-month~1]
+ @ParameterizedTest
+ @CsvSource({ //
+ 0L + ", '0-00'", //
+ 11L + ", '0-11'", //
+ 999999999L * 12 + ", '999999999-00'", //
+ 999999999L * 12 + 11 + ", '999999999-11'", //
+ 1L * 12 + 1 + ", '1-01'" //
+ })
+ void testOfMonths(final long value, final String expected) {
+ assertThat(IntervalYearToMonth.ofMonths(value).toString(), equalTo(expected));
+ }
+
+ // [utest->dsn~exasol.parsing-interval-year-to-month-from-strings~1]
+ @ParameterizedTest
+ @CsvSource({ "'0-0', '0-00'", //
+ "'1-2', '1-02'", //
+ "'22-11', '22-11'", //
+ "'999999999-11', '999999999-11'" //
+ })
+ void testParse(final String text, final String expected) {
+ assertThat(IntervalYearToMonth.parse(text).toString(), equalTo(expected));
+ }
+
+ // [utest->dsn~exasol.parsing-interval-year-to-month-from-strings~1]
+ @ParameterizedTest
+ @ValueSource(strings = { "0", "-0", "0-", "0-123", "1000000000-0" })
+ void testParseIllegalInputThrowsException(final String text) {
+ assertThrows(IllegalArgumentException.class, () -> IntervalYearToMonth.parse(text));
+ }
+}
\ No newline at end of file