From 42eb923aebcdf6aaa1bbc0d540aeecf913c08358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 09:17:45 +0200 Subject: [PATCH 1/8] PMI-97: Convert and parse INTERVALS. --- .classpath | 45 ++++++--- doc/design.md | 34 +++++++ doc/system_requirements.md | 39 +++++--- pom.xml | 6 ++ .../interval/IntervalDayToSecond.java | 99 +++++++++++++++++++ .../datatype/interval/TestInterval.java | 47 +++++++++ 6 files changed, 241 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java create mode 100644 src/test/java/com/exasol/datatype/interval/TestInterval.java diff --git a/.classpath b/.classpath index c3b4b716..ebfacabc 100644 --- a/.classpath +++ b/.classpath @@ -1,15 +1,34 @@ - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/design.md b/doc/design.md index aa444da3..580a4e20 100644 --- a/doc/design.md +++ b/doc/design.md @@ -112,4 +112,38 @@ Covers: * `req~rendering.sql.confiugrable-identifier-quoting~1` +Needs: impl, utest + +### Exasol Dialect Specific + +#### Converting from 64 bit Integers to INTERVAL DAY TO SECOND +`dsn~exasol.converting-int-to-interval-day-to-second~1` + +The data converter converts integers to `INTERVAL DAY TO SECOND`. + +Covers: + +* `req~integer-interval-conversion~1` + +Needs: impl, utest + +#### Parsing INTERVAL DAY TO SECOND From Strings +`dsn~exasol.parsing-interval-day-to-second-from-strings~1` + +The data converter can parse `INTERVAL DAY TO SECOND` from strings in the following format: + + inverval-d2s = [ days SP ] hours ":" minutes [ ":" seconds [ "." milliseconds ] ] + + hours = "2" "0" - "3" / [ "0" / "1" ] DIGIT + + minutes = "5" DIGIT / [ "0" - "4" ] DIGIT + + seconds = "5" DIGIT / [ "0" - "4" ] DIGIT + + milliseconds = 1*3DIGIT + +Covers: + +* `req~integer-interval-conversion~1` + Needs: impl, utest \ No newline at end of file diff --git a/doc/system_requirements.md b/doc/system_requirements.md index 109c6efd..824be8d3 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -84,6 +84,17 @@ Making sure at compile time that illegal constructs do not compile make the resu Needs: req +### Data Conversion +`feat~data-conversion~1` + +ESB converts between values of compatible data types. + +Rationale: + +Different databases and related tools use different ways to store and process similar data types. A collection of well-tested converters saves the API users time and trouble. + +Needs: req + ## Functional Requirements ### Statement Structure @@ -202,7 +213,7 @@ Covers: Needs: dsn -###### Configurable Identifier Quoting +#### Configurable Identifier Quoting `req~rendering.sql.confiugrable-identifier-quoting~1` ESB allows users to choose whether the following identifiers should be quoted in the rendered query: @@ -221,10 +232,6 @@ Covers: Needs: dsn -#### TODO - -* One line / pretty - #### SELECT Statement Rendering `req~rendering.sql.select~1` @@ -247,19 +254,19 @@ Covers: Needs: dsn -### TODO +### Exasol Dialect Specific Requirements + +###### Integer - Interval Conversion +`req~integer-interval-conversion~1` ---- +ESB converts values of type `INTERVAL` to integer and vice-versa. -SELECT -* Fields -* Asterisk ("*") +Rationale: + +Neighboring systems of an Exasol database often do to have equivalent data types, so conversion to a primitive data type is required. -FROM +Covers: -( INNER / ( LEFT / RIGHT / FULL ) OUTER ) JOIN -* ON +* [feat~data-conversion~1](#data-conversion) -LIMIT -* offset -* count +Needs: dsn \ No newline at end of file diff --git a/pom.xml b/pom.xml index efa0541c..d891ef7e 100644 --- a/pom.xml +++ b/pom.xml @@ -31,6 +31,12 @@ ${junit.platform.version} test + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + 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..d77f5eea --- /dev/null +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -0,0 +1,99 @@ +package com.exasol.datatype.interval; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +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 final long value; + 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 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 milliseconds + * @return interval + */ + public static IntervalDayToSecond ofMillis(final long value) { + return new IntervalDayToSecond(value); + } + + /** + * Parse an {@link IntervalDayToSecond} from a string + * + * @param text + * @return + */ + public static IntervalDayToSecond parse(final String text) { + return new IntervalDayToSecond(text); + } +} \ No newline at end of file diff --git a/src/test/java/com/exasol/datatype/interval/TestInterval.java b/src/test/java/com/exasol/datatype/interval/TestInterval.java new file mode 100644 index 00000000..915f907e --- /dev/null +++ b/src/test/java/com/exasol/datatype/interval/TestInterval.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 TestInterval { + // [utest->req~integer-interval-conversion~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 testMillisecondsToIntervalDayToSecond(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 From fafa12f5e380ba7ba56ea747af1e3b5a134f4d21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 12:29:09 +0200 Subject: [PATCH 2/8] Added missing documentation. --- .../com/exasol/datatype/interval/IntervalDayToSecond.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java index d77f5eea..4907a250 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -3,6 +3,10 @@ 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. + */ public class IntervalDayToSecond { private static final long MILLIS_PER_SECOND = 1000L; private static final long SECONDS_PER_MINUTE = 60L; @@ -11,7 +15,6 @@ public class IntervalDayToSecond { 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 final long value; private static final int DAYS_MATCHING_GROUP = 1; private static final int HOURS_MATCHING_GROUP = 2; private static final int MINUTES_MATCHING_GROUP = 3; @@ -23,6 +26,7 @@ public class IntervalDayToSecond { + "(?::(\\d{1,2})" // seconds + "(?:\\.(\\d{1,3}))?)?" // milliseconds ); + private final long value; private IntervalDayToSecond(final long value) { this.value = value; From aaf4bf6ba7a2055e647ea093f3391b4512c1e0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 13:00:25 +0200 Subject: [PATCH 3/8] PMI-97: Improved API documentation. --- .../interval/IntervalDayToSecond.java | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java index 4907a250..0e682801 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -6,6 +6,21 @@ /** * 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 expresses 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; @@ -84,8 +99,8 @@ private long getMillis() { /** * Create an {@link IntervalDayToSecond} from a number of milliseconds * - * @param value milliseconds - * @return interval + * @param value total length of the interval in milliseconds + * @return interval with milliseconds resolution */ public static IntervalDayToSecond ofMillis(final long value) { return new IntervalDayToSecond(value); @@ -94,8 +109,24 @@ public static IntervalDayToSecond ofMillis(final long value) { /** * Parse an {@link IntervalDayToSecond} from a string * - * @param text - * @return + *

+ * 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 */ public static IntervalDayToSecond parse(final String text) { return new IntervalDayToSecond(text); From c06e3b49c5fd642f6f26b0cec4aa25d8541ef9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 13:09:16 +0200 Subject: [PATCH 4/8] PMI-97: Improved API documentation. --- .../interval/IntervalDayToSecond.java | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java index 0e682801..492a3333 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -113,17 +113,22 @@ public static IntervalDayToSecond ofMillis(final long value) { * The accepted format is: *

*

- * [dddddddd ]hh:mm[:ss[.SSS]] + * [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
  • - *
+ *
+ *
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 From 19c0face7f295714f8010da7270e19c9802f0f6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 14:05:44 +0200 Subject: [PATCH 5/8] PMI-97: Fixed requirement trace. --- .../java/com/exasol/datatype/interval/IntervalDayToSecond.java | 2 ++ src/test/java/com/exasol/datatype/interval/TestInterval.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java index 492a3333..905e55e3 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -102,6 +102,7 @@ private long getMillis() { * @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); } @@ -133,6 +134,7 @@ public static IntervalDayToSecond ofMillis(final long value) { * @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); } diff --git a/src/test/java/com/exasol/datatype/interval/TestInterval.java b/src/test/java/com/exasol/datatype/interval/TestInterval.java index 915f907e..5b4f04ea 100644 --- a/src/test/java/com/exasol/datatype/interval/TestInterval.java +++ b/src/test/java/com/exasol/datatype/interval/TestInterval.java @@ -9,7 +9,7 @@ import org.junit.jupiter.params.provider.ValueSource; class TestInterval { - // [utest->req~integer-interval-conversion~1] + // [utest->dsn~exasol.converting-int-to-interval-day-to-second~1] @ParameterizedTest @CsvSource({ // 0L + ", '0:00:00.000'", // From 9eef6c27162de399ee52a17b1ea31e953b0db66e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 14:42:36 +0200 Subject: [PATCH 6/8] PMI-97: Added support for INTERVAL YEAR TO MONTH. Fixed documentation --- doc/design.md | 36 ++++++- .../interval/IntervalDayToSecond.java | 2 +- .../interval/IntervalYearToMonth.java | 95 +++++++++++++++++++ ...rval.java => TestIntervalDayToSecond.java} | 4 +- .../interval/TestIntervalYearToMonth.java | 42 ++++++++ 5 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java rename src/test/java/com/exasol/datatype/interval/{TestInterval.java => TestIntervalDayToSecond.java} (94%) create mode 100644 src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java diff --git a/doc/design.md b/doc/design.md index 580a4e20..4e4368f3 100644 --- a/doc/design.md +++ b/doc/design.md @@ -11,7 +11,7 @@ The Data Query Language (DQL) building block is responsible for managing `SELECT ### Fluent Programming -###### Statement Construction With Fluent Programming +#### Statement Construction With Fluent Programming `dsn~statement-construction-with-fluent-programming~1` All statement builders use the "fluent programming" model, where the return type of each builder step determines the possible next structural elements that can be added. @@ -134,11 +134,11 @@ The data converter can parse `INTERVAL DAY TO SECOND` from strings in the follow inverval-d2s = [ days SP ] hours ":" minutes [ ":" seconds [ "." milliseconds ] ] - hours = "2" "0" - "3" / [ "0" / "1" ] DIGIT + hours = ( "2" "0" - "3" ) / ( [ "0" / "1" ] DIGIT ) - minutes = "5" DIGIT / [ "0" - "4" ] DIGIT + minutes = ( "5" DIGIT ) / ( [ "0" - "4" ] DIGIT ) - seconds = "5" DIGIT / [ "0" - "4" ] DIGIT + seconds = ( "5" DIGIT ) / ( [ "0" - "4" ] DIGIT ) milliseconds = 1*3DIGIT @@ -146,4 +146,32 @@ Covers: * `req~integer-interval-conversion~1` +Needs: impl, utest + +#### Converting from 64 bit Integers to INTERVAL YEAR TO MONTH +`dsn~exasol.converting-int-to-interval-year-to-month~1` + +The data converter converts integers to `INTERVAL YEAR TO MONTH`. + +Covers: + +* `req~integer-interval-conversion~1` + +Needs: impl, utest + +#### Parsing INTERVAL YEAR TO MONTH From Strings +`dsn~exasol.parsing-interval-year-to-month-from-strings~1` + +The data converter can parse `INTERVAL YEAR TO MONTH` from strings in the following format: + + inverval-y2m = days "-" months + + days = 1*9DIGIT + + months = ( "1" "0" - "2" ) / DIGIT + +Covers: + +* `req~integer-interval-conversion~1` + Needs: impl, utest \ No newline at end of file diff --git a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java index 905e55e3..bb55524b 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalDayToSecond.java @@ -18,7 +18,7 @@ *
  • milliseconds (or fraction of seconds)
  • * * - * Since milliseconds are the highest resolution, each interval can also be expresses as a total number of milliseconds. + * 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. */ 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..14e1dbc3 --- /dev/null +++ b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java @@ -0,0 +1,95 @@ +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: + *

    + *
      + *
    • years
    • + *
    • months
    • + *
    + * + * 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 + */ + 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 + */ + 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/TestInterval.java b/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java similarity index 94% rename from src/test/java/com/exasol/datatype/interval/TestInterval.java rename to src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java index 5b4f04ea..285b5461 100644 --- a/src/test/java/com/exasol/datatype/interval/TestInterval.java +++ b/src/test/java/com/exasol/datatype/interval/TestIntervalDayToSecond.java @@ -8,7 +8,7 @@ import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.ValueSource; -class TestInterval { +class TestIntervalDayToSecond { // [utest->dsn~exasol.converting-int-to-interval-day-to-second~1] @ParameterizedTest @CsvSource({ // @@ -20,7 +20,7 @@ class TestInterval { 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 testMillisecondsToIntervalDayToSecond(final long value, final String expected) { + void testofMillis(final long value, final String expected) { assertThat(IntervalDayToSecond.ofMillis(value).toString(), equalTo(expected)); } 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..03cff591 --- /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-day-to-second-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 From 804a4cf5555eb7778919a6f2807fe3ba22d933db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 14:56:45 +0200 Subject: [PATCH 7/8] PMI-97: Fixed requirement trace. --- .../java/com/exasol/datatype/interval/IntervalYearToMonth.java | 2 ++ .../com/exasol/datatype/interval/TestIntervalYearToMonth.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java index 14e1dbc3..613a615a 100644 --- a/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java +++ b/src/main/java/com/exasol/datatype/interval/IntervalYearToMonth.java @@ -64,6 +64,7 @@ private long getMonths() { * @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); } @@ -89,6 +90,7 @@ public static IntervalYearToMonth ofMonths(final long value) { * @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); } diff --git a/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java index 03cff591..8f25d81e 100644 --- a/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java +++ b/src/test/java/com/exasol/datatype/interval/TestIntervalYearToMonth.java @@ -33,7 +33,7 @@ void testParse(final String text, final String expected) { assertThat(IntervalYearToMonth.parse(text).toString(), equalTo(expected)); } - // [utest->dsn~exasol.parsing-interval-day-to-second-from-strings~1] + // [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) { From 091fa79a75bee57795e2a27b2ac90ef95345d343 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20B=C3=A4r?= Date: Tue, 16 Oct 2018 15:04:00 +0200 Subject: [PATCH 8/8] PMI-97: Worked in review findings of @bobkodex. --- doc/design.md | 8 ++++++-- doc/system_requirements.md | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/doc/design.md b/doc/design.md index 4e4368f3..1b8837ef 100644 --- a/doc/design.md +++ b/doc/design.md @@ -132,7 +132,7 @@ Needs: impl, utest The data converter can parse `INTERVAL DAY TO SECOND` from strings in the following format: - inverval-d2s = [ days SP ] hours ":" minutes [ ":" seconds [ "." milliseconds ] ] + interval-d2s = [ days SP ] hours ":" minutes [ ":" seconds [ "." milliseconds ] ] hours = ( "2" "0" - "3" ) / ( [ "0" / "1" ] DIGIT ) @@ -142,6 +142,8 @@ The data converter can parse `INTERVAL DAY TO SECOND` from strings in the follow milliseconds = 1*3DIGIT +Examples are `12:30`, `12:30.081` or `100 12:30:00.081`. + Covers: * `req~integer-interval-conversion~1` @@ -164,12 +166,14 @@ Needs: impl, utest The data converter can parse `INTERVAL YEAR TO MONTH` from strings in the following format: - inverval-y2m = days "-" months + interval-y2m = days "-" months days = 1*9DIGIT months = ( "1" "0" - "2" ) / DIGIT +Examples are `0-1` and `100-11`. + Covers: * `req~integer-interval-conversion~1` diff --git a/doc/system_requirements.md b/doc/system_requirements.md index 824be8d3..f6287272 100644 --- a/doc/system_requirements.md +++ b/doc/system_requirements.md @@ -263,7 +263,7 @@ ESB converts values of type `INTERVAL` to integer and vice-versa. Rationale: -Neighboring systems of an Exasol database often do to have equivalent data types, so conversion to a primitive data type is required. +Neighboring systems of an Exasol database often do not have equivalent data types, so conversion to a primitive data type is required. Covers: