From 0c89d9d16f179035a1d74f5b0a3e74d9251211a3 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Sat, 6 Apr 2019 08:31:06 +0200 Subject: [PATCH 1/3] optimized date parsing Date parsing using a regular expression is slower than a specific implementation for the only format that is supported. --- .../src/main/java/com/google/cloud/Date.java | 18 ++-- .../main/java/com/google/cloud/IntParser.java | 82 +++++++++++++++++++ .../test/java/com/google/cloud/DateTest.java | 43 +++++++++- .../java/com/google/cloud/IntParserTest.java | 68 +++++++++++++++ 4 files changed, 197 insertions(+), 14 deletions(-) create mode 100644 google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/IntParser.java create mode 100644 google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/IntParserTest.java diff --git a/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java b/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java index 442e7dca0eef..3ddce3ec5c6b 100644 --- a/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java +++ b/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java @@ -21,15 +21,12 @@ import java.io.Serializable; import java.util.Calendar; import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** Represents a Date without time, such as 2017-03-17. Date is timezone independent. */ @BetaApi("This is going to be replaced with LocalDate from threetenbp") public final class Date implements Comparable, Serializable { // Date format "yyyy-mm-dd" - private static final Pattern FORMAT_REGEXP = Pattern.compile("(\\d\\d\\d\\d)-(\\d\\d)-(\\d\\d)"); private static final long serialVersionUID = 8067099123096783929L; private final int year; private final int month; @@ -57,13 +54,14 @@ public static Date fromYearMonthDay(int year, int month, int dayOfMonth) { /** @param date Data in RFC 3339 date format (yyyy-mm-dd). */ public static Date parseDate(String date) { - Matcher matcher = FORMAT_REGEXP.matcher(date); - if (!matcher.matches()) { - throw new IllegalArgumentException("Invalid date: " + date); - } - int year = Integer.parseInt(matcher.group(1)); - int month = Integer.parseInt(matcher.group(2)); - int dayOfMonth = Integer.parseInt(matcher.group(3)); + Preconditions.checkNotNull(date); + final String invalidDate = "Invalid date: " + date; + Preconditions.checkArgument(date.length() == 10, invalidDate); + Preconditions.checkArgument(date.charAt(4) == '-', invalidDate); + Preconditions.checkArgument(date.charAt(7) == '-', invalidDate); + int year = IntParser.parseInt(date, 0, 4); + int month = IntParser.parseInt(date, 5, 7); + int dayOfMonth = IntParser.parseInt(date, 8, 10); return new Date(year, month, dayOfMonth); } diff --git a/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/IntParser.java b/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/IntParser.java new file mode 100644 index 000000000000..e9ecfa8eea7e --- /dev/null +++ b/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/IntParser.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import com.google.common.base.Preconditions; + +/** + * Util class for fast parsing of integer values. This is used by {@link Date#parseDate(String)} and + * {@link Timestamp#parseTimestamp(String)}. These parse methods are used internally by Google + * client libraries to parse text values returned by services, and these parse methods should be as + * efficient as possible. + */ +class IntParser { + + private static final int[] POWERS_OF_10 = { + 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 + }; + + /** Parses an int from the given input between the specified begin and end. */ + static int parseInt(String input, int begin, int end) { + return parseInt(input, begin, end, 0); + } + + /** + * Parses an int from the given input between the specified begin and end. The value is multiplied + * by 10^exponent. The exponent must be between 0 and 10 inclusive. + */ + static int parseInt(String input, int begin, int end, int exponent) { + Preconditions.checkNotNull(input); + Preconditions.checkArgument( + exponent >= 0 && exponent < POWERS_OF_10.length, "Exponent out of range"); + Preconditions.checkArgument(end - begin <= 10, "Max input length is 10"); + Preconditions.checkArgument(end >= begin, "End must be greater or equal to begin"); + Preconditions.checkArgument(begin >= 0, "Begin must be >= 0"); + Preconditions.checkArgument(end <= input.length(), "End must be <= input.length()"); + int res = 0; + for (int index = begin; index < end; index++) { + res += parseDigit(input.charAt(index), input) * POWERS_OF_10[end - index - 1]; + } + return res * POWERS_OF_10[exponent]; + } + + private static int parseDigit(char c, String input) { + switch (c) { + case '0': + return 0; + case '1': + return 1; + case '2': + return 2; + case '3': + return 3; + case '4': + return 4; + case '5': + return 5; + case '6': + return 6; + case '7': + return 7; + case '8': + return 8; + case '9': + return 9; + } + throw new NumberFormatException("Not a decimal digit: " + c); + } +} diff --git a/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java b/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java index 5892cc718b01..7f699ca1c77f 100644 --- a/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java +++ b/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java @@ -18,6 +18,7 @@ import static com.google.common.testing.SerializableTester.reserializeAndAssert; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; import com.google.common.testing.EqualsTester; import java.text.ParseException; @@ -34,10 +35,44 @@ public class DateTest { @Test public void parseDate() { - Date date = Date.parseDate("2016-09-18"); - assertThat(date.getYear()).isEqualTo(2016); - assertThat(date.getMonth()).isEqualTo(9); - assertThat(date.getDayOfMonth()).isEqualTo(18); + verifyDate("2016-09-18", 2016, 9, 18); + verifyDate("2000-01-01", 2000, 1, 1); + verifyDate("9999-12-31", 9999, 12, 31); + verifyDate("0001-01-01", 1, 1, 1); + verifyDate("2000-02-29", 2000, 2, 29); // This is a valid leap year. + verifyDate("1900-02-29", 1900, 2, 29); // This is NOT a valid leap year. + verifyDate("2001-02-29", 2001, 2, 29); // Also not a valid leap year. + verifyDate("2000-04-31", 2000, 4, 31); // Not a valid date. + } + + private void verifyDate(String input, int year, int month, int day) { + Date date = Date.parseDate(input); + assertThat(date.getYear()).isEqualTo(year); + assertThat(date.getMonth()).isEqualTo(month); + assertThat(date.getDayOfMonth()).isEqualTo(day); + } + + @Test + public void parseInvalidDates() { + parseInvalidDate("2016/09/18"); + parseInvalidDate("2016 09 18"); + parseInvalidDate("2016-9-18"); + parseInvalidDate("2016-09-18T10:00"); + parseInvalidDate(""); + parseInvalidDate("test"); + parseInvalidDate("2000-13-01"); + parseInvalidDate("2000-12-32"); + parseInvalidDate("10000-01-01"); + parseInvalidDate("0000-01-01"); + } + + private void parseInvalidDate(String input) { + try { + Date.parseDate(input); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("Invalid"); + } } @Test diff --git a/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/IntParserTest.java b/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/IntParserTest.java new file mode 100644 index 000000000000..34cefd26dd32 --- /dev/null +++ b/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/IntParserTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class IntParserTest { + + @Test + public void testParse() { + assertThat(IntParser.parseInt("1", 0, 1)).isEqualTo(1); + assertThat(IntParser.parseInt("1234", 0, 4)).isEqualTo(1234); + assertThat(IntParser.parseInt("1234", 2, 4)).isEqualTo(34); + assertThat(IntParser.parseInt("1234", 0, 2)).isEqualTo(12); + assertThat(IntParser.parseInt("1234567890", 0, 10)).isEqualTo(1234567890); + assertThat(IntParser.parseInt("1234567890", 1, 10)).isEqualTo(234567890); + assertThat(IntParser.parseInt("0123456789", 0, 10)).isEqualTo(123456789); + assertThat(IntParser.parseInt("00001234", 0, 8)).isEqualTo(1234); + assertThat(IntParser.parseInt("", 0, 0)).isEqualTo(0); + parseInvalidNumber("test", 0, 4); + parseInvalidNumber("123T456", 0, 4); + parseInvalidArgument("", 0, 1); + parseInvalidArgument("1234", 0, 5); + parseInvalidArgument("1234", -1, 4); + parseInvalidArgument("1234", 3, 2); + // Roman literal 50 is a valid numeric character, but not a decimal digit. + char c = '\u216C'; + int val = Character.getNumericValue(c); + assertThat(val).isEqualTo(50); + parseInvalidArgument(String.valueOf(c), 0, 1); + } + + private void parseInvalidNumber(String input, int begin, int end) { + try { + IntParser.parseInt(input, begin, end); + fail("Expected exception"); + } catch (NumberFormatException e) { + } + } + + private void parseInvalidArgument(String input, int begin, int end) { + try { + IntParser.parseInt(input, begin, end); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + } + } +} From 2a4a707f92dbde05ab3e3ca03f2bbd0359450969 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Sun, 7 Apr 2019 09:29:27 +0200 Subject: [PATCH 2/3] removed IntParser and added test cases --- .../src/main/java/com/google/cloud/Date.java | 6 +- .../main/java/com/google/cloud/IntParser.java | 82 ------------------- .../test/java/com/google/cloud/DateTest.java | 9 +- .../java/com/google/cloud/IntParserTest.java | 68 --------------- 4 files changed, 11 insertions(+), 154 deletions(-) delete mode 100644 google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/IntParser.java delete mode 100644 google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/IntParserTest.java diff --git a/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java b/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java index 3ddce3ec5c6b..07ff08446367 100644 --- a/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java +++ b/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java @@ -59,9 +59,9 @@ public static Date parseDate(String date) { Preconditions.checkArgument(date.length() == 10, invalidDate); Preconditions.checkArgument(date.charAt(4) == '-', invalidDate); Preconditions.checkArgument(date.charAt(7) == '-', invalidDate); - int year = IntParser.parseInt(date, 0, 4); - int month = IntParser.parseInt(date, 5, 7); - int dayOfMonth = IntParser.parseInt(date, 8, 10); + int year = Integer.parseInt(date.substring(0, 4)); + int month = Integer.parseInt(date.substring(5, 7)); + int dayOfMonth = Integer.parseInt(date.substring(8, 10)); return new Date(year, month, dayOfMonth); } diff --git a/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/IntParser.java b/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/IntParser.java deleted file mode 100644 index e9ecfa8eea7e..000000000000 --- a/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/IntParser.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud; - -import com.google.common.base.Preconditions; - -/** - * Util class for fast parsing of integer values. This is used by {@link Date#parseDate(String)} and - * {@link Timestamp#parseTimestamp(String)}. These parse methods are used internally by Google - * client libraries to parse text values returned by services, and these parse methods should be as - * efficient as possible. - */ -class IntParser { - - private static final int[] POWERS_OF_10 = { - 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000 - }; - - /** Parses an int from the given input between the specified begin and end. */ - static int parseInt(String input, int begin, int end) { - return parseInt(input, begin, end, 0); - } - - /** - * Parses an int from the given input between the specified begin and end. The value is multiplied - * by 10^exponent. The exponent must be between 0 and 10 inclusive. - */ - static int parseInt(String input, int begin, int end, int exponent) { - Preconditions.checkNotNull(input); - Preconditions.checkArgument( - exponent >= 0 && exponent < POWERS_OF_10.length, "Exponent out of range"); - Preconditions.checkArgument(end - begin <= 10, "Max input length is 10"); - Preconditions.checkArgument(end >= begin, "End must be greater or equal to begin"); - Preconditions.checkArgument(begin >= 0, "Begin must be >= 0"); - Preconditions.checkArgument(end <= input.length(), "End must be <= input.length()"); - int res = 0; - for (int index = begin; index < end; index++) { - res += parseDigit(input.charAt(index), input) * POWERS_OF_10[end - index - 1]; - } - return res * POWERS_OF_10[exponent]; - } - - private static int parseDigit(char c, String input) { - switch (c) { - case '0': - return 0; - case '1': - return 1; - case '2': - return 2; - case '3': - return 3; - case '4': - return 4; - case '5': - return 5; - case '6': - return 6; - case '7': - return 7; - case '8': - return 8; - case '9': - return 9; - } - throw new NumberFormatException("Not a decimal digit: " + c); - } -} diff --git a/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java b/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java index 7f699ca1c77f..12ee82f9472f 100644 --- a/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java +++ b/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java @@ -60,10 +60,17 @@ public void parseInvalidDates() { parseInvalidDate("2016-09-18T10:00"); parseInvalidDate(""); parseInvalidDate("test"); + parseInvalidDate("aaaa-bb-cc"); + parseInvalidDate("aaaa-01-01"); + parseInvalidDate("2019-bb-01"); + parseInvalidDate("2019-01-cc"); parseInvalidDate("2000-13-01"); parseInvalidDate("2000-12-32"); parseInvalidDate("10000-01-01"); parseInvalidDate("0000-01-01"); + parseInvalidDate("-001-01-01"); + parseInvalidDate("0001--1-01"); + parseInvalidDate("0001-01--1"); } private void parseInvalidDate(String input) { @@ -71,7 +78,7 @@ private void parseInvalidDate(String input) { Date.parseDate(input); fail("Expected exception"); } catch (IllegalArgumentException e) { - assertThat(e.getMessage()).contains("Invalid"); + // Ignore, this is the expected exception. } } diff --git a/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/IntParserTest.java b/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/IntParserTest.java deleted file mode 100644 index 34cefd26dd32..000000000000 --- a/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/IntParserTest.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2019 Google LLC - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.google.cloud; - -import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.fail; - -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.JUnit4; - -@RunWith(JUnit4.class) -public class IntParserTest { - - @Test - public void testParse() { - assertThat(IntParser.parseInt("1", 0, 1)).isEqualTo(1); - assertThat(IntParser.parseInt("1234", 0, 4)).isEqualTo(1234); - assertThat(IntParser.parseInt("1234", 2, 4)).isEqualTo(34); - assertThat(IntParser.parseInt("1234", 0, 2)).isEqualTo(12); - assertThat(IntParser.parseInt("1234567890", 0, 10)).isEqualTo(1234567890); - assertThat(IntParser.parseInt("1234567890", 1, 10)).isEqualTo(234567890); - assertThat(IntParser.parseInt("0123456789", 0, 10)).isEqualTo(123456789); - assertThat(IntParser.parseInt("00001234", 0, 8)).isEqualTo(1234); - assertThat(IntParser.parseInt("", 0, 0)).isEqualTo(0); - parseInvalidNumber("test", 0, 4); - parseInvalidNumber("123T456", 0, 4); - parseInvalidArgument("", 0, 1); - parseInvalidArgument("1234", 0, 5); - parseInvalidArgument("1234", -1, 4); - parseInvalidArgument("1234", 3, 2); - // Roman literal 50 is a valid numeric character, but not a decimal digit. - char c = '\u216C'; - int val = Character.getNumericValue(c); - assertThat(val).isEqualTo(50); - parseInvalidArgument(String.valueOf(c), 0, 1); - } - - private void parseInvalidNumber(String input, int begin, int end) { - try { - IntParser.parseInt(input, begin, end); - fail("Expected exception"); - } catch (NumberFormatException e) { - } - } - - private void parseInvalidArgument(String input, int begin, int end) { - try { - IntParser.parseInt(input, begin, end); - fail("Expected exception"); - } catch (IllegalArgumentException e) { - } - } -} From bce525d1ea4289c517b27d7f5332d2367825b216 Mon Sep 17 00:00:00 2001 From: Olav Loite Date: Tue, 9 Apr 2019 11:33:38 +0200 Subject: [PATCH 3/3] check for invalid date/year/month/day --- .../src/main/java/com/google/cloud/Date.java | 12 ++++-- .../test/java/com/google/cloud/DateTest.java | 43 ++++++++++++++++--- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java b/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java index 07ff08446367..d7f3b71275e7 100644 --- a/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java +++ b/google-cloud-clients/google-cloud-core/src/main/java/com/google/cloud/Date.java @@ -59,10 +59,14 @@ public static Date parseDate(String date) { Preconditions.checkArgument(date.length() == 10, invalidDate); Preconditions.checkArgument(date.charAt(4) == '-', invalidDate); Preconditions.checkArgument(date.charAt(7) == '-', invalidDate); - int year = Integer.parseInt(date.substring(0, 4)); - int month = Integer.parseInt(date.substring(5, 7)); - int dayOfMonth = Integer.parseInt(date.substring(8, 10)); - return new Date(year, month, dayOfMonth); + try { + int year = Integer.parseInt(date.substring(0, 4)); + int month = Integer.parseInt(date.substring(5, 7)); + int dayOfMonth = Integer.parseInt(date.substring(8, 10)); + return new Date(year, month, dayOfMonth); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(invalidDate, e); + } } /** diff --git a/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java b/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java index 12ee82f9472f..14b6a139d004 100644 --- a/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java +++ b/google-cloud-clients/google-cloud-core/src/test/java/com/google/cloud/DateTest.java @@ -64,13 +64,15 @@ public void parseInvalidDates() { parseInvalidDate("aaaa-01-01"); parseInvalidDate("2019-bb-01"); parseInvalidDate("2019-01-cc"); - parseInvalidDate("2000-13-01"); - parseInvalidDate("2000-12-32"); + parseInvalidMonth("2000-13-01"); + parseInvalidMonth("2000-00-01"); + parseInvalidDay("2000-12-32"); + parseInvalidDay("2000-12-00"); parseInvalidDate("10000-01-01"); - parseInvalidDate("0000-01-01"); - parseInvalidDate("-001-01-01"); - parseInvalidDate("0001--1-01"); - parseInvalidDate("0001-01--1"); + parseInvalidYear("0000-01-01"); + parseInvalidYear("-001-01-01"); + parseInvalidMonth("0001--1-01"); + parseInvalidDay("0001-01--1"); } private void parseInvalidDate(String input) { @@ -78,7 +80,34 @@ private void parseInvalidDate(String input) { Date.parseDate(input); fail("Expected exception"); } catch (IllegalArgumentException e) { - // Ignore, this is the expected exception. + assertThat(e.getMessage()).contains("Invalid date"); + } + } + + private void parseInvalidYear(String input) { + try { + Date.parseDate(input); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("Invalid year"); + } + } + + private void parseInvalidMonth(String input) { + try { + Date.parseDate(input); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("Invalid month"); + } + } + + private void parseInvalidDay(String input) { + try { + Date.parseDate(input); + fail("Expected exception"); + } catch (IllegalArgumentException e) { + assertThat(e.getMessage()).contains("Invalid day"); } }