Skip to content

Commit 881a783

Browse files
committed
chore: fix scientific notation bug
1 parent d8a4d0e commit 881a783

4 files changed

Lines changed: 78 additions & 65 deletions

File tree

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtility.java

Lines changed: 44 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package com.google.cloud.bigquery.jdbc;
1818

1919
import com.google.common.base.Strings;
20+
import java.math.BigDecimal;
21+
import java.math.RoundingMode;
2022
import java.sql.Date;
2123
import java.sql.Time;
2224
import java.sql.Timestamp;
@@ -143,45 +145,34 @@ public static Timestamp boxTimestamp(String val) {
143145
}
144146
}
145147

148+
public static Instant parseEpochDecimalToInstant(String epochDecimal) {
149+
if (epochDecimal == null) {
150+
return null;
151+
}
152+
BigDecimal bd = new BigDecimal(epochDecimal);
153+
long seconds = bd.setScale(0, RoundingMode.FLOOR).longValue();
154+
long nanos =
155+
bd.subtract(BigDecimal.valueOf(seconds))
156+
.movePointRight(9)
157+
.setScale(0, RoundingMode.DOWN)
158+
.longValue();
159+
return Instant.ofEpochSecond(seconds, nanos);
160+
}
161+
146162
public static String formatTimestampString(String epochDecimal, boolean enableTimestampPicos) {
147163
if (epochDecimal == null) {
148164
return null;
149165
}
150166

151-
int dotIdx = epochDecimal.indexOf('.');
152-
long seconds;
153-
String fraction;
167+
BigDecimal bd = new BigDecimal(epochDecimal);
168+
long seconds = bd.setScale(0, RoundingMode.FLOOR).longValue();
169+
BigDecimal fractionalSeconds = bd.subtract(BigDecimal.valueOf(seconds));
154170

155-
if (dotIdx == -1) {
156-
seconds = Long.parseLong(epochDecimal);
157-
fraction = "000000";
158-
} else {
159-
boolean isNegative = epochDecimal.startsWith("-");
160-
long wholeSeconds = Long.parseLong(epochDecimal.substring(0, dotIdx));
161-
fraction = epochDecimal.substring(dotIdx + 1);
162-
163-
if (!enableTimestampPicos && fraction.length() > 6) {
164-
fraction = fraction.substring(0, 6);
165-
}
166-
fraction = Strings.padEnd(fraction, 6, '0');
171+
int originalScale = bd.scale() > 0 ? bd.scale() : 0;
172+
int scale = enableTimestampPicos ? Math.max(6, Math.min(12, originalScale)) : 6;
167173

168-
if (isNegative) {
169-
long fracVal = Long.parseLong(fraction);
170-
if (fracVal > 0) {
171-
int len = fraction.length();
172-
long divisor = 1;
173-
for (int i = 0; i < len; i++) {
174-
divisor *= 10;
175-
}
176-
seconds = wholeSeconds - 1;
177-
fraction = Strings.padStart(Long.toString(divisor - fracVal), len, '0');
178-
} else {
179-
seconds = wholeSeconds;
180-
}
181-
} else {
182-
seconds = wholeSeconds;
183-
}
184-
}
174+
String fraction =
175+
fractionalSeconds.setScale(scale, RoundingMode.DOWN).toPlainString().substring(2);
185176

186177
Instant instant = Instant.ofEpochSecond(seconds);
187178
return UTC_FORMATTER.format(instant) + "." + fraction;
@@ -198,31 +189,36 @@ public static String formatTimestampStringFromMicroseconds(long microseconds) {
198189

199190
public static String formatTimestampStringFromIso(
200191
String isoString, boolean enableTimestampPicos) {
201-
String result = isoString;
192+
if (isoString == null) {
193+
return null;
194+
}
202195

203-
if (result.endsWith("Z")) {
204-
result = result.substring(0, result.length() - 1);
205-
} else if (result.endsWith(" UTC")) {
206-
result = result.substring(0, result.length() - 4);
196+
String s = isoString;
197+
if (s.endsWith(" UTC")) {
198+
s = s.substring(0, s.length() - 4);
199+
} else if (s.endsWith("Z")) {
200+
s = s.substring(0, s.length() - 1);
207201
}
208202

209-
if (result.length() > 10 && result.charAt(10) == 'T') {
210-
result = result.substring(0, 10) + ' ' + result.substring(11);
203+
if (s.length() > 10 && s.charAt(10) == 'T') {
204+
s = s.substring(0, 10) + ' ' + s.substring(11);
211205
}
212206

213-
int dotIdx = result.indexOf('.');
207+
int dotIdx = s.indexOf('.');
214208
if (dotIdx == -1) {
215-
return result + ".000000";
209+
return s + ".000000";
216210
}
217211

218-
int fractionLen = result.length() - dotIdx - 1;
219-
if (!enableTimestampPicos && fractionLen > 6) {
220-
return result.substring(0, dotIdx + 7);
221-
}
212+
String base = s.substring(0, dotIdx);
213+
String fraction = s.substring(dotIdx + 1);
222214

223-
if (fractionLen < 6) {
224-
result = Strings.padEnd(result, result.length() + (6 - fractionLen), '0');
215+
int maxScale = enableTimestampPicos ? 12 : 6;
216+
if (fraction.length() > maxScale) {
217+
fraction = fraction.substring(0, maxScale);
218+
} else if (fraction.length() < 6) {
219+
fraction = Strings.padEnd(fraction, 6, '0');
225220
}
226-
return result;
221+
222+
return base + "." + fraction;
227223
}
228224
}

java-bigquery-jdbc/src/main/java/com/google/cloud/bigquery/jdbc/BigQueryTypeCoercionUtility.java

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import com.google.cloud.bigquery.FieldValue;
2121
import com.google.cloud.bigquery.FieldValue.Attribute;
2222
import com.google.cloud.bigquery.Range;
23-
import com.google.common.base.Strings;
2423
import java.math.BigDecimal;
2524
import java.sql.Date;
2625
import java.sql.Time;
@@ -447,22 +446,9 @@ public Timestamp coerce(FieldValue fieldValue) {
447446
// Timestamp.valueOf() expects "yyyy-mm-dd hh:mm:ss.fffffffff" format.
448447
return Timestamp.valueOf(rawValue.replace('T', ' '));
449448
} else {
450-
// It's a TIMESTAMP numeric epoch decimal string (e.g. "1775642400.123456789123")
451-
int dotIdx = rawValue.indexOf('.');
452-
long seconds;
453-
long nanos = 0;
454-
if (dotIdx == -1) {
455-
seconds = Long.parseLong(rawValue);
456-
} else {
457-
seconds = Long.parseLong(rawValue.substring(0, dotIdx));
458-
String fraction = rawValue.substring(dotIdx + 1);
459-
if (fraction.length() > 9) {
460-
fraction = fraction.substring(0, 9);
461-
}
462-
fraction = Strings.padEnd(fraction, 9, '0');
463-
nanos = Long.parseLong(fraction);
464-
}
465-
Instant instant = Instant.ofEpochSecond(seconds, nanos);
449+
// Numeric epoch decimal string from BigQuery JSON (e.g. "1775642400.123456789123" or
450+
// "1.6905474E9")
451+
Instant instant = BigQueryTemporalUtility.parseEpochDecimalToInstant(rawValue);
466452
// Timezone-agnostic conversion preserving exact point in time as mandated by JDBC spec
467453
return Timestamp.from(instant);
468454
}

java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/BigQueryTemporalUtilityTest.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import static com.google.common.truth.Truth.assertThat;
2020

21+
import java.time.Instant;
2122
import org.junit.jupiter.api.Test;
2223

2324
public class BigQueryTemporalUtilityTest {
@@ -40,6 +41,10 @@ public void testFormatTimestampString() {
4041
.isEqualTo("1969-12-31 23:59:59.000000");
4142
assertThat(BigQueryTemporalUtility.formatTimestampString("-0.123456789123", true))
4243
.isEqualTo("1969-12-31 23:59:59.876543210877");
44+
assertThat(BigQueryTemporalUtility.formatTimestampString("1.6905474E9", false))
45+
.isEqualTo("2023-07-28 12:30:00.000000");
46+
assertThat(BigQueryTemporalUtility.formatTimestampString("1.690547400123456E9", true))
47+
.isEqualTo("2023-07-28 12:30:00.123456");
4348
}
4449

4550
@Test
@@ -76,4 +81,22 @@ public void testBoxTimestamp() {
7681
BigQueryTemporalUtility.boxTimestamp("2026-04-08 10:00:00.123456789123");
7782
assertThat(tsFallback.getNanos()).isEqualTo(123456789);
7883
}
84+
85+
@Test
86+
public void testParseEpochDecimalToInstant() {
87+
// Standard decimal
88+
Instant i1 = BigQueryTemporalUtility.parseEpochDecimalToInstant("1775642400.123456789123");
89+
assertThat(i1).isEqualTo(Instant.ofEpochSecond(1775642400, 123456789));
90+
91+
// Scientific notation
92+
Instant i2 = BigQueryTemporalUtility.parseEpochDecimalToInstant("1.6905474E9");
93+
assertThat(i2).isEqualTo(Instant.parse("2023-07-28T12:30:00Z"));
94+
95+
// Pre-1970 negative decimal
96+
Instant i3 = BigQueryTemporalUtility.parseEpochDecimalToInstant("-0.123456");
97+
assertThat(i3).isEqualTo(Instant.ofEpochSecond(-1, 876544000));
98+
99+
// Null
100+
assertThat(BigQueryTemporalUtility.parseEpochDecimalToInstant(null)).isNull();
101+
}
79102
}

java-bigquery-jdbc/src/test/java/com/google/cloud/bigquery/jdbc/FieldValueTypeBigQueryCoercionUtilityTest.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,14 @@ public void fieldValueToTimestampWithNanos() {
314314
assertThat(result.getNanos()).isEqualTo(123456789);
315315
}
316316

317+
@Test
318+
public void fieldValueToTimestampScientificNotation() {
319+
FieldValue scientificValue = FieldValue.of(PRIMITIVE, "1.6905474E9");
320+
Timestamp result = INSTANCE.coerceTo(Timestamp.class, scientificValue);
321+
assertThat(result).isNotNull();
322+
assertThat(result).isEqualTo(Timestamp.valueOf("2023-07-28 12:30:00"));
323+
}
324+
317325
@Test
318326
public void fieldValueToTimestampWhenNull() {
319327
assertThat(INSTANCE.coerceTo(Timestamp.class, null)).isNull();

0 commit comments

Comments
 (0)