Skip to content

Commit

Permalink
feat: add support for converting interval fields to threeten PeriodDu…
Browse files Browse the repository at this point in the history
…ration (#2838)

Add support for converting BigQuery interval type to threeten PeriodDuration, information about the canonical interval form can be found [here](https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#canonical_format_3). Parsing logic was referenced from [here](https://cloud.google.com/bigquery/docs/reference/standard-sql/interval_functions#extract).

Fixes #1849 ☕️
  • Loading branch information
obada-ab committed Sep 6, 2023
1 parent 989d997 commit 2294c2f
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 3 deletions.
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -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"
```
<!-- {x-version-update-end} -->

Expand Down Expand Up @@ -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
Expand Down
Expand Up @@ -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
Expand Down Expand Up @@ -237,6 +243,28 @@ public List<FieldValue> getRepeatedValue() {
return (List<FieldValue>) 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
Expand Down Expand Up @@ -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}.
*
* <p>The parameter {@code interval} should be an interval in the canonical format: "[sign]Y-M
* [sign]D [sign]H:M:S[.F]". More details <a href=
* "https://cloud.google.com/bigquery/docs/reference/standard-sql/data-types#canonical_format_3">
* here</a>
*
* @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(
"(?<sign1>[+-])?(?<year>\\d+)-(?<month>\\d+) (?<sign2>[-|+])?(?<day>\\d+) (?<sign3>[-|+])?(?<hours>\\d+):(?<minutes>\\d+):(?<seconds>\\d+)(\\.(?<fraction>\\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));
}
}
Expand Up @@ -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 {

Expand All @@ -43,6 +48,10 @@ public class FieldValueTest {
ImmutableMap.of("v", "123456789.123456789");
private static final Map<String, String> STRING_FIELD = ImmutableMap.of("v", "string");
private static final Map<String, String> TIMESTAMP_FIELD = ImmutableMap.of("v", "42");
private static final Map<String, String> INTERVAL_FIELD_1 =
ImmutableMap.of("v", "P3Y2M1DT12H34M56.789S");
private static final Map<String, String> INTERVAL_FIELD_2 =
ImmutableMap.of("v", "3-2 1 12:34:56.789");
private static final Map<String, String> BYTES_FIELD = ImmutableMap.of("v", BYTES_BASE64);
private static final Map<String, String> NULL_FIELD =
ImmutableMap.of("v", Data.nullOf(String.class));
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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<String, PeriodDuration> 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<String, PeriodDuration> entry : intervalToPeriodDuration.entrySet()) {
assertEquals(FieldValue.parseCanonicalInterval(entry.getKey()), entry.getValue());
System.out.println(FieldValue.parseCanonicalInterval(entry.getKey()));
}
}
}
Expand Up @@ -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));
Expand Down

0 comments on commit 2294c2f

Please sign in to comment.