diff --git a/README.md b/README.md index aceecda14..1665fb7d1 100644 --- a/README.md +++ b/README.md @@ -60,13 +60,13 @@ implementation 'com.google.cloud:google-cloud-bigquery' If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-bigquery:2.31.1' +implementation 'com.google.cloud:google-cloud-bigquery:2.31.2' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-bigquery" % "2.31.1" +libraryDependencies += "com.google.cloud" % "google-cloud-bigquery" % "2.31.2" ``` @@ -351,7 +351,7 @@ Java is a registered trademark of Oracle and/or its affiliates. [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-bigquery/java11.html [stability-image]: https://img.shields.io/badge/stability-stable-green [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-bigquery.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigquery/2.31.1 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-bigquery/2.31.2 [authentication]: https://github.com/googleapis/google-cloud-java#authentication [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles diff --git a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/FieldValue.java b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/FieldValue.java index 58c012a60..ea68075f7 100644 --- a/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/FieldValue.java +++ b/google-cloud-bigquery/src/main/java/com/google/cloud/bigquery/FieldValue.java @@ -27,10 +27,16 @@ import java.io.Serializable; import java.math.BigDecimal; import java.math.RoundingMode; +import java.time.Duration; import java.time.Instant; +import java.time.Period; +import java.time.format.DateTimeParseException; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import org.threeten.extra.PeriodDuration; /** * Google BigQuery Table Field Value class. Objects of this class represent values of a BigQuery @@ -237,6 +243,28 @@ public List getRepeatedValue() { return (List) value; } + /** + * Returns this field's value as a {@link org.threeten.extra.PeriodDuration}. This method should + * be used if the corresponding field has {@link StandardSQLTypeName#INTERVAL} type, or if it is a + * legal canonical format "[sign]Y-M [sign]D [sign]H:M:S[.F]", e.g. "123-7 -19 0:24:12.000006" or + * ISO 8601. + * + * @throws ClassCastException if the field is not a primitive type + * @throws NullPointerException if {@link #isNull()} returns {@code true} + * @throws IllegalArgumentException if the field cannot be converted to a legal interval + */ + @SuppressWarnings("unchecked") + public PeriodDuration getPeriodDuration() { + checkNotNull(value); + try { + // Try parsing from ISO 8601 + return PeriodDuration.parse(getStringValue()); + } catch (DateTimeParseException dateTimeParseException) { + // Try parsing from canonical interval format + return parseCanonicalInterval(getStringValue()); + } + } + /** * Returns this field's value as a {@link FieldValueList} instance. This method should only be * used if the corresponding field has {@link LegacySQLTypeName#RECORD} type (i.e. {@link @@ -325,4 +353,63 @@ static FieldValue fromPb(Object cellPb, Field recordSchema) { } throw new IllegalArgumentException("Unexpected table cell format"); } + + /** + * Parse interval in canonical format and create instance of {@code PeriodDuration}. + * + *

The parameter {@code interval} should be an interval in the canonical format: "[sign]Y-M + * [sign]D [sign]H:M:S[.F]". More details + * here + * + * @throws IllegalArgumentException if the {@code interval} is not a valid interval + */ + static PeriodDuration parseCanonicalInterval(String interval) throws IllegalArgumentException { + // Pattern is [sign]Y-M [sign]D [sign]H:M:S[.F] + Pattern pattern = + Pattern.compile( + "(?[+-])?(?\\d+)-(?\\d+) (?[-|+])?(?\\d+) (?[-|+])?(?\\d+):(?\\d+):(?\\d+)(\\.(?\\d+))?"); + Matcher matcher = pattern.matcher(interval); + if (!matcher.find()) { + throw new IllegalArgumentException(); + } + String sign1 = matcher.group("sign1"); + String year = matcher.group("year"); + String month = matcher.group("month"); + String sign2 = matcher.group("sign2"); + String day = matcher.group("day"); + String sign3 = matcher.group("sign3"); + String hours = matcher.group("hours"); + String minutes = matcher.group("minutes"); + String seconds = matcher.group("seconds"); + String fraction = matcher.group("fraction"); + + int yearInt = Integer.parseInt(year); + int monthInt = Integer.parseInt(month); + if (Objects.equals(sign1, "-")) { + yearInt *= -1; + monthInt *= -1; + } + + int dayInt = Integer.parseInt(day); + if (Objects.equals(sign2, "-")) { + dayInt *= -1; + } + if (sign3 == null) { + sign3 = ""; + } + + String durationString = + sign3 + + "PT" + + hours + + "H" + + minutes + + "M" + + seconds + + (fraction == null ? "" : "." + fraction) + + "S"; + + return PeriodDuration.of(Period.of(yearInt, monthInt, dayInt), Duration.parse(durationString)); + } } diff --git a/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/FieldValueTest.java b/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/FieldValueTest.java index e4ec47b47..90cb69061 100644 --- a/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/FieldValueTest.java +++ b/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/FieldValueTest.java @@ -27,8 +27,13 @@ import com.google.common.collect.ImmutableMap; import com.google.common.io.BaseEncoding; import java.math.BigDecimal; +import java.time.Duration; +import java.time.Period; +import java.util.LinkedHashMap; import java.util.Map; +import java.util.Map.Entry; import org.junit.Test; +import org.threeten.extra.PeriodDuration; public class FieldValueTest { @@ -43,6 +48,10 @@ public class FieldValueTest { ImmutableMap.of("v", "123456789.123456789"); private static final Map STRING_FIELD = ImmutableMap.of("v", "string"); private static final Map TIMESTAMP_FIELD = ImmutableMap.of("v", "42"); + private static final Map INTERVAL_FIELD_1 = + ImmutableMap.of("v", "P3Y2M1DT12H34M56.789S"); + private static final Map INTERVAL_FIELD_2 = + ImmutableMap.of("v", "3-2 1 12:34:56.789"); private static final Map BYTES_FIELD = ImmutableMap.of("v", BYTES_BASE64); private static final Map NULL_FIELD = ImmutableMap.of("v", Data.nullOf(String.class)); @@ -74,6 +83,17 @@ public void testFromPb() { value = FieldValue.fromPb(TIMESTAMP_FIELD); assertEquals(FieldValue.Attribute.PRIMITIVE, value.getAttribute()); assertEquals(42000000, value.getTimestampValue()); + value = FieldValue.fromPb(INTERVAL_FIELD_1); + assertEquals(FieldValue.Attribute.PRIMITIVE, value.getAttribute()); + PeriodDuration periodDuration = + PeriodDuration.of(Period.of(3, 2, 1), Duration.parse("PT12H34M56.789S")); + assertEquals(periodDuration, value.getPeriodDuration()); + assertEquals("P3Y2M1DT12H34M56.789S", value.getStringValue()); + value = FieldValue.fromPb(INTERVAL_FIELD_2); + assertEquals(FieldValue.Attribute.PRIMITIVE, value.getAttribute()); + periodDuration = PeriodDuration.of(Period.of(3, 2, 1), Duration.parse("PT12H34M56.789S")); + assertEquals(periodDuration, value.getPeriodDuration()); + assertEquals("3-2 1 12:34:56.789", value.getStringValue()); value = FieldValue.fromPb(BYTES_FIELD); assertEquals(FieldValue.Attribute.PRIMITIVE, value.getAttribute()); assertArrayEquals(BYTES, value.getBytesValue()); @@ -146,4 +166,22 @@ public void testEquals() { assertEquals(recordValue, FieldValue.fromPb(RECORD_FIELD)); assertEquals(recordValue.hashCode(), FieldValue.fromPb(RECORD_FIELD).hashCode()); } + + @Test + public void testParseCanonicalInterval() { + Map intervalToPeriodDuration = new LinkedHashMap<>(); + intervalToPeriodDuration.put( + "125-7 -19 -0:24:12.001", PeriodDuration.parse("P125Y7M-19DT0H-24M-12.001S")); + intervalToPeriodDuration.put("-15-6 23 23:14:05", PeriodDuration.parse("P-15Y-6M23DT23H14M5S")); + intervalToPeriodDuration.put( + "06-01 06 01:01:00.123456", PeriodDuration.parse("P6Y1M6DT1H1M0.123456S")); + intervalToPeriodDuration.put("-0-0 -0 -0:0:0", PeriodDuration.parse("P0Y0M0DT0H0M0S")); + intervalToPeriodDuration.put( + "-99999-99999 9999 999:999:999.999999999", + PeriodDuration.parse("P-99999Y-99999M9999DT999H999M999.999999999S")); + for (Entry entry : intervalToPeriodDuration.entrySet()) { + assertEquals(FieldValue.parseCanonicalInterval(entry.getKey()), entry.getValue()); + System.out.println(FieldValue.parseCanonicalInterval(entry.getKey())); + } + } } diff --git a/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java b/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java index bb9c233a4..cf180f9a3 100644 --- a/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java +++ b/google-cloud-bigquery/src/test/java/com/google/cloud/bigquery/it/ITBigQueryTest.java @@ -1228,8 +1228,11 @@ public void testIntervalType() throws InterruptedException { .build(); TableResult result = bigquery.query(queryJobConfiguration); assertNotNull(result.getJobId()); + PeriodDuration periodDuration = + PeriodDuration.of(Period.of(125, 7, -19), java.time.Duration.parse("PT24M12.000006S")); for (FieldValueList values : result.iterateAll()) { assertEquals("125-7 -19 0:24:12.000006", values.get(0).getValue()); + assertEquals(periodDuration, values.get(0).getPeriodDuration()); } } finally { assertTrue(bigquery.delete(tableId));