From e9d0ed5c6777d8f8ad60ce73551ab377f4830b2a Mon Sep 17 00:00:00 2001 From: Alexey Andreev Date: Wed, 6 May 2020 10:44:51 +0300 Subject: [PATCH] java.time: reduce generated code size --- checkstyle.xml | 6 +- .../main/java/org/threeten/bp/Duration.java | 215 +++++++++++++----- .../main/java/org/threeten/bp/Instant.java | 7 +- .../main/java/org/threeten/bp/MonthDay.java | 17 +- .../src/main/java/org/threeten/bp/Period.java | 152 ++++++++++--- .../src/main/java/org/threeten/bp/Year.java | 12 +- .../main/java/org/threeten/bp/YearMonth.java | 16 +- .../main/java/org/threeten/bp/ZoneRegion.java | 41 +++- .../bp/format/DateTimeFormatterBuilder.java | 4 +- .../classlib/java/util/LinkedHashMapTest.java | 8 +- 10 files changed, 338 insertions(+), 140 deletions(-) diff --git a/checkstyle.xml b/checkstyle.xml index a6bbf8e3a..aced76b08 100644 --- a/checkstyle.xml +++ b/checkstyle.xml @@ -1,7 +1,11 @@ - + + + + + diff --git a/classlib/src/main/java/org/threeten/bp/Duration.java b/classlib/src/main/java/org/threeten/bp/Duration.java index 0d597f4b9..a325d492a 100644 --- a/classlib/src/main/java/org/threeten/bp/Duration.java +++ b/classlib/src/main/java/org/threeten/bp/Duration.java @@ -61,8 +61,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.threeten.bp.format.DateTimeParseException; import org.threeten.bp.jdk8.Jdk8Methods; import org.threeten.bp.temporal.ChronoField; @@ -117,15 +115,7 @@ public final class Duration /** * Constant for nanos per second. */ - private static final BigInteger BI_NANOS_PER_SECOND = BigInteger.valueOf(NANOS_PER_SECOND); - /** - * The pattern for parsing. - */ - // TODO: get rid of regexp - private final static Pattern PATTERN = - Pattern.compile("([-+]?)P(?:([-+]?[0-9]+)D)?" - + "(T(?:([-+]?[0-9]+)H)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)(?:[.,]([0-9]{0,9}))?S)?)?", - Pattern.CASE_INSENSITIVE); + private static BigInteger bigIntNanosPerSecond; /** * The number of seconds in the duration. @@ -137,6 +127,13 @@ public final class Duration */ private final int nanos; + private static BigInteger getBigIntNanosPerSecond() { + if (bigIntNanosPerSecond == null) { + bigIntNanosPerSecond = BigInteger.valueOf(NANOS_PER_SECOND); + } + return bigIntNanosPerSecond; + } + //----------------------------------------------------------------------- /** * Obtains an instance of {@code Duration} from a number of standard 24 hour days. @@ -398,60 +395,156 @@ public final class Duration */ public static Duration parse(CharSequence text) { Objects.requireNonNull(text, "text"); - Matcher matcher = PATTERN.matcher(text); - if (matcher.matches()) { - // check for letter T but no time sections - if (!"T".equals(matcher.group(3))) { - boolean negate = "-".equals(matcher.group(1)); - String dayMatch = matcher.group(2); - String hourMatch = matcher.group(4); - String minuteMatch = matcher.group(5); - String secondMatch = matcher.group(6); - String fractionMatch = matcher.group(7); - if (dayMatch != null || hourMatch != null || minuteMatch != null || secondMatch != null) { - long daysAsSecs = parseNumber(text, dayMatch, SECONDS_PER_DAY, "days"); - long hoursAsSecs = parseNumber(text, hourMatch, SECONDS_PER_HOUR, "hours"); - long minsAsSecs = parseNumber(text, minuteMatch, SECONDS_PER_MINUTE, "minutes"); - long seconds = parseNumber(text, secondMatch, 1, "seconds"); - boolean negativeSecs = secondMatch != null && secondMatch.charAt(0) == '-'; - int nanos = parseFraction(text, fractionMatch, negativeSecs ? -1 : 1); - try { - return create(negate, daysAsSecs, hoursAsSecs, minsAsSecs, seconds, nanos); - } catch (ArithmeticException ex) { - throw new DateTimeParseException("Text cannot be parsed to a Duration: overflow", text, 0, ex); - } + Parser parser = new Parser(text); + if (!parser.parse() || !parser.hasOneField) { + throw new DateTimeParseException("Text cannot be parsed to a Duration", text, parser.ptr); + } + return create(parser.negative, parser.days * SECONDS_PER_DAY, parser.hours * SECONDS_PER_HOUR, + parser.minutes * SECONDS_PER_MINUTE, parser.seconds, parser.nanos); + } + + static class Parser { + private int ptr; + private CharSequence text; + private int days; + private int hours; + private int minutes; + private int seconds; + private int nanos; + private boolean negative; + private boolean hasOneField; + private int parsedNumber; + + Parser(CharSequence text) { + this.text = text; + } + + boolean parse() { + negative = sign(); + if (eof() || text.charAt(ptr) != 'P') { + return false; + } + ptr++; + + if (eof()) { + return false; + } + + if (text.charAt(ptr) != 'T') { + if (!tryParseDays()) { + return false; + } + if (eof()) { + return true; + } + if (text.charAt(ptr) != 'T') { + return false; + } + ++ptr; + hasOneField = false; + } else { + ++ptr; + } + + int state = 0; + loop: do { + if (!number()) { + break; + } + if (eof()) { + return false; + } + hasOneField = true; + char c = text.charAt(ptr); + + //CHECKSTYLE.OFF: FallThrough + switch (state) { + case 0: + if (c == 'H') { + ++ptr; + hours = parsedNumber; + state = 1; + break; + } + case 1: + if (c == 'M') { + ++ptr; + minutes = parsedNumber; + state = 2; + break; + } + case 2: + if (c == 'S') { + ++ptr; + seconds = parsedNumber; + break loop; + } else if (c == '.') { + seconds = parsedNumber; + if (!number()) { + return false; + } + nanos = parsedNumber; + if (eof() || text.charAt(ptr) != 'S') { + return false; + } + ++ptr; + break loop; + } + default: + return false; + } + //CHECKSTYLE.ON: FallThrough + } while (true); + + return eof() && hasOneField; + } + + private boolean tryParseDays() { + if (!number()) { + return false; + } + days = parsedNumber; + hasOneField = true; + if (ptr >= text.length() || text.charAt(ptr) != 'D') { + return false; + } + ++ptr; + return true; + } + + boolean eof() { + return ptr >= text.length(); + } + + boolean sign() { + if (!eof()) { + if (text.charAt(ptr) == '-') { + ptr++; + return true; + } else if (text.charAt(ptr) == '+') { + ptr++; } } + return false; } - throw new DateTimeParseException("Text cannot be parsed to a Duration", text, 0); - } - private static long parseNumber(CharSequence text, String parsed, int multiplier, String errorText) { - // regex limits to [-+]?[0-9]+ - if (parsed == null) { - return 0; - } - try { - if (parsed.startsWith("+")) { - parsed = parsed.substring(1); + boolean number() { + boolean negative = sign(); + parsedNumber = 0; + boolean hasDigits = false; + while (ptr < text.length()) { + char c = text.charAt(ptr); + if (c < '0' || c >= '9') { + break; + } + ++ptr; + hasDigits = true; + parsedNumber = parsedNumber * 10 + c - '0'; } - long val = Long.parseLong(parsed); - return Jdk8Methods.safeMultiply(val, multiplier); - } catch (NumberFormatException | ArithmeticException ex) { - throw new DateTimeParseException("Text cannot be parsed to a Duration: " + errorText, text, 0, ex); - } - } - - private static int parseFraction(CharSequence text, String parsed, int negate) { - // regex limits to [0-9]{0,9} - if (parsed == null || parsed.length() == 0) { - return 0; - } - try { - parsed = (parsed + "000000000").substring(0, 9); - return Integer.parseInt(parsed) * negate; - } catch (NumberFormatException | ArithmeticException ex) { - throw new DateTimeParseException("Text cannot be parsed to a Duration: fraction", text, 0, ex); + if (negative) { + parsedNumber = -parsedNumber; + } + return hasDigits; } } @@ -951,7 +1044,7 @@ public final class Duration */ private static Duration create(BigDecimal seconds) { BigInteger nanos = seconds.movePointRight(9).toBigIntegerExact(); - BigInteger[] divRem = nanos.divideAndRemainder(BI_NANOS_PER_SECOND); + BigInteger[] divRem = nanos.divideAndRemainder(getBigIntNanosPerSecond()); if (divRem[0].bitLength() > 63) { throw new ArithmeticException("Exceeds capacity of Duration: " + nanos); } diff --git a/classlib/src/main/java/org/threeten/bp/Instant.java b/classlib/src/main/java/org/threeten/bp/Instant.java index 66c0a8247..1cd3eb75f 100644 --- a/classlib/src/main/java/org/threeten/bp/Instant.java +++ b/classlib/src/main/java/org/threeten/bp/Instant.java @@ -58,7 +58,9 @@ import static org.threeten.bp.temporal.ChronoUnit.NANOS; import java.io.Serializable; import java.util.Objects; import org.threeten.bp.format.DateTimeFormatter; +import org.threeten.bp.format.DateTimeFormatterBuilder; import org.threeten.bp.format.DateTimeParseException; +import org.threeten.bp.format.DateTimePrintContext; import org.threeten.bp.jdk8.Jdk8Methods; import org.threeten.bp.temporal.ChronoField; import org.threeten.bp.temporal.ChronoUnit; @@ -1166,6 +1168,9 @@ public final class Instant */ @Override public String toString() { - return DateTimeFormatter.ISO_INSTANT.format(this); + StringBuilder sb = new StringBuilder(); + new DateTimeFormatterBuilder.InstantPrinterParser(-2).print( + new DateTimePrintContext(this, null, null), sb); + return sb.toString(); } } diff --git a/classlib/src/main/java/org/threeten/bp/MonthDay.java b/classlib/src/main/java/org/threeten/bp/MonthDay.java index 7e633118e..d22b4490b 100644 --- a/classlib/src/main/java/org/threeten/bp/MonthDay.java +++ b/classlib/src/main/java/org/threeten/bp/MonthDay.java @@ -96,15 +96,6 @@ import org.threeten.bp.temporal.ValueRange; */ public final class MonthDay implements TemporalAccessor, TemporalAdjuster, Comparable, Serializable { - /** - * Parser. - */ - private static final DateTimeFormatter PARSER = new DateTimeFormatterBuilder() - .appendLiteral("--") - .appendValue(MONTH_OF_YEAR, 2) - .appendLiteral('-') - .appendValue(DAY_OF_MONTH, 2) - .toFormatter(); /** * The month-of-year, not null. @@ -254,7 +245,13 @@ public final class MonthDay * @throws DateTimeParseException if the text cannot be parsed */ public static MonthDay parse(CharSequence text) { - return parse(text, PARSER); + // TODO: get rid of DateTimeFormatterBuilder + return parse(text, new DateTimeFormatterBuilder() + .appendLiteral("--") + .appendValue(MONTH_OF_YEAR, 2) + .appendLiteral('-') + .appendValue(DAY_OF_MONTH, 2) + .toFormatter()); } /** diff --git a/classlib/src/main/java/org/threeten/bp/Period.java b/classlib/src/main/java/org/threeten/bp/Period.java index cd3e66bcc..9658613e9 100644 --- a/classlib/src/main/java/org/threeten/bp/Period.java +++ b/classlib/src/main/java/org/threeten/bp/Period.java @@ -54,8 +54,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.threeten.bp.chrono.ChronoLocalDate; import org.threeten.bp.chrono.ChronoPeriod; import org.threeten.bp.chrono.Chronology; @@ -110,12 +108,6 @@ public final class Period * A constant for a period of zero. */ public static final Period ZERO = new Period(0, 0, 0); - /** - * The pattern for parsing. - */ - private final static Pattern PATTERN = - Pattern.compile("([-+]?)P(?:([-+]?[0-9]+)Y)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)W)?(?:([-+]?[0-9]+)D)?", - Pattern.CASE_INSENSITIVE); /** * The number of years. @@ -312,38 +304,126 @@ public final class Period */ public static Period parse(CharSequence text) { Objects.requireNonNull(text, "text"); - Matcher matcher = PATTERN.matcher(text); - if (matcher.matches()) { - int negate = "-".equals(matcher.group(1)) ? -1 : 1; - String yearMatch = matcher.group(2); - String monthMatch = matcher.group(3); - String weekMatch = matcher.group(4); - String dayMatch = matcher.group(5); - if (yearMatch != null || monthMatch != null || weekMatch != null || dayMatch != null) { - try { - int years = parseNumber(text, yearMatch, negate); - int months = parseNumber(text, monthMatch, negate); - int weeks = parseNumber(text, weekMatch, negate); - int days = parseNumber(text, dayMatch, negate); - days = Jdk8Methods.safeAdd(days, Jdk8Methods.safeMultiply(weeks, 7)); - return create(years, months, days); - } catch (NumberFormatException ex) { - throw new DateTimeParseException("Text cannot be parsed to a Period", text, 0, ex); - } - } + Parser parser = new Parser(text); + if (!parser.parse() || !parser.hasOneField) { + throw new DateTimeParseException("Text cannot be parsed to a Period", text, parser.ptr); } - throw new DateTimeParseException("Text cannot be parsed to a Period", text, 0); + if (parser.negative) { + parser.years = -parser.years; + parser.months = -parser.months; + parser.weeks = -parser.weeks; + parser.days = -parser.days; + } + int days = Jdk8Methods.safeAdd(parser.days, Jdk8Methods.safeMultiply(parser.weeks, 7)); + return create(parser.years, parser.months, days); } - private static int parseNumber(CharSequence text, String str, int negate) { - if (str == null) { - return 0; + static class Parser { + private int ptr; + private CharSequence text; + private int years; + private int months; + private int weeks; + private int days; + private boolean negative; + private boolean hasOneField; + private int parsedNumber; + + Parser(CharSequence text) { + this.text = text; } - int val = Integer.parseInt(str); - try { - return Jdk8Methods.safeMultiply(val, negate); - } catch (ArithmeticException ex) { - throw new DateTimeParseException("Text cannot be parsed to a Period", text, 0, ex); + + boolean parse() { + negative = sign(); + if (eof() || text.charAt(ptr) != 'P') { + return false; + } + ptr++; + + if (eof()) { + return false; + } + + int state = 0; + while (number()) { + if (eof()) { + return false; + } + hasOneField = true; + char c = text.charAt(ptr); + + //CHECKSTYLE.OFF: FallThrough + switch (state) { + case 0: + if (c == 'Y') { + ++ptr; + years = parsedNumber; + state = 1; + break; + } + case 1: + if (c == 'M') { + ++ptr; + months = parsedNumber; + state = 2; + break; + } + case 2: + if (c == 'W') { + ++ptr; + weeks = parsedNumber; + state = 3; + break; + } + case 3: + if (c == 'D') { + ++ptr; + days = parsedNumber; + state = 4; + break; + } + default: + return false; + } + //CHECKSTYLE.ON: FallThrough + } + + return eof() && hasOneField; + } + + boolean eof() { + return ptr >= text.length(); + } + + boolean sign() { + if (!eof()) { + if (text.charAt(ptr) == '-') { + ptr++; + return true; + } else if (text.charAt(ptr) == '+') { + ptr++; + } + } + return false; + } + + boolean number() { + boolean negative = sign(); + parsedNumber = 0; + boolean hasDigits = false; + while (ptr < text.length()) { + char c = text.charAt(ptr); + if (c < '0' || c >= '9') { + break; + } + ++ptr; + hasDigits = true; + parsedNumber = parsedNumber * 10 + c - '0'; + } + if (negative) { + parsedNumber = -parsedNumber; + } + return hasDigits; } } diff --git a/classlib/src/main/java/org/threeten/bp/Year.java b/classlib/src/main/java/org/threeten/bp/Year.java index 2453a26df..3a97b1b51 100644 --- a/classlib/src/main/java/org/threeten/bp/Year.java +++ b/classlib/src/main/java/org/threeten/bp/Year.java @@ -115,13 +115,6 @@ public final class Year */ public static final int MAX_VALUE = 999999999; - /** - * Parser. - */ - private static final DateTimeFormatter PARSER = new DateTimeFormatterBuilder() - .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD) - .toFormatter(); - /** * The year being represented. */ @@ -239,7 +232,10 @@ public final class Year * @throws DateTimeParseException if the text cannot be parsed */ public static Year parse(CharSequence text) { - return parse(text, PARSER); + // TODO: Get rid of DateTimeFormatterBuilder + return parse(text, new DateTimeFormatterBuilder() + .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .toFormatter()); } /** diff --git a/classlib/src/main/java/org/threeten/bp/YearMonth.java b/classlib/src/main/java/org/threeten/bp/YearMonth.java index e1174f49e..66d4878e0 100644 --- a/classlib/src/main/java/org/threeten/bp/YearMonth.java +++ b/classlib/src/main/java/org/threeten/bp/YearMonth.java @@ -102,15 +102,6 @@ import org.threeten.bp.temporal.ValueRange; public final class YearMonth implements Temporal, TemporalAdjuster, Comparable, Serializable, TemporalAccessor { - /** - * Parser. - */ - private static final DateTimeFormatter PARSER = new DateTimeFormatterBuilder() - .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD) - .appendLiteral('-') - .appendValue(MONTH_OF_YEAR, 2) - .toFormatter(); - /** * The year. */ @@ -243,7 +234,12 @@ public final class YearMonth * @throws DateTimeParseException if the text cannot be parsed */ public static YearMonth parse(CharSequence text) { - return parse(text, PARSER); + // TODO: get rid of format + return parse(text, new DateTimeFormatterBuilder() + .appendValue(YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral('-') + .appendValue(MONTH_OF_YEAR, 2) + .toFormatter()); } /** diff --git a/classlib/src/main/java/org/threeten/bp/ZoneRegion.java b/classlib/src/main/java/org/threeten/bp/ZoneRegion.java index eda77149c..26421b8df 100644 --- a/classlib/src/main/java/org/threeten/bp/ZoneRegion.java +++ b/classlib/src/main/java/org/threeten/bp/ZoneRegion.java @@ -48,7 +48,6 @@ package org.threeten.bp; import java.io.Serializable; import java.util.Objects; -import java.util.regex.Pattern; import org.threeten.bp.zone.ZoneRules; import org.threeten.bp.zone.ZoneRulesException; import org.threeten.bp.zone.ZoneRulesProvider; @@ -73,11 +72,6 @@ import org.threeten.bp.zone.ZoneRulesProvider; */ final class ZoneRegion extends ZoneId implements Serializable { - /** - * The regex pattern for region IDs. - */ - private static final Pattern PATTERN = Pattern.compile("[A-Za-z][A-Za-z0-9~/._+-]+"); - /** * The time-zone ID, not null. */ @@ -139,7 +133,7 @@ final class ZoneRegion extends ZoneId implements Serializable { */ static ZoneRegion ofId(String zoneId, boolean checkAvailable) { Objects.requireNonNull(zoneId, "zoneId"); - if (zoneId.length() < 2 || !PATTERN.matcher(zoneId).matches()) { + if (!isValidId(zoneId)) { throw new DateTimeException("Invalid ID for region-based ZoneId, invalid format: " + zoneId); } ZoneRules rules = null; @@ -157,6 +151,39 @@ final class ZoneRegion extends ZoneId implements Serializable { return new ZoneRegion(zoneId, rules); } + private static boolean isValidId(String id) { + if (id.length() < 2) { + return false; + } + if (!isIdStart(id.charAt(0))) { + return false; + } + for (int i = 1; i < id.length(); ++i) { + if (!isIdPart(id.charAt(i))) { + return false; + } + } + return true; + } + + private static boolean isIdPart(char c) { + switch (c) { + case '~': + case '/': + case '.': + case '_': + case '+': + case '-': + return true; + default: + return isIdStart(c) || c >= '0' && c <= '9'; + } + } + + private static boolean isIdStart(char c) { + return c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'; + } + //------------------------------------------------------------------------- /** * Constructor. diff --git a/classlib/src/main/java/org/threeten/bp/format/DateTimeFormatterBuilder.java b/classlib/src/main/java/org/threeten/bp/format/DateTimeFormatterBuilder.java index 2eeae6d1b..a1334122e 100644 --- a/classlib/src/main/java/org/threeten/bp/format/DateTimeFormatterBuilder.java +++ b/classlib/src/main/java/org/threeten/bp/format/DateTimeFormatterBuilder.java @@ -2931,7 +2931,7 @@ public final class DateTimeFormatterBuilder { /** * Prints or parses an ISO-8601 instant. */ - static final class InstantPrinterParser implements DateTimePrinterParser { + public static final class InstantPrinterParser implements DateTimePrinterParser { // days in a 400 year cycle = 146097 // days in a 10,000 year cycle = 146097 * 25 // seconds per day = 86400 @@ -2940,7 +2940,7 @@ public final class DateTimeFormatterBuilder { private final int fractionalDigits; - InstantPrinterParser(int fractionalDigits) { + public InstantPrinterParser(int fractionalDigits) { this.fractionalDigits = fractionalDigits; } diff --git a/tests/src/test/java/org/teavm/classlib/java/util/LinkedHashMapTest.java b/tests/src/test/java/org/teavm/classlib/java/util/LinkedHashMapTest.java index 5d5d1927c..5a07ec1d4 100644 --- a/tests/src/test/java/org/teavm/classlib/java/util/LinkedHashMapTest.java +++ b/tests/src/test/java/org/teavm/classlib/java/util/LinkedHashMapTest.java @@ -111,9 +111,9 @@ public class LinkedHashMapTest { assertNull("Empty LinkedHashMap access", empty.get("nothing")); empty.put("something", "here"); - //CHECKSTYLE:OFF + //CHECKSTYLE.OFF:StringLiteralEquality assertTrue("cannot get element", empty.get("something") == "here"); - //CHECKSTYLE:ON + //CHECKSTYLE.ON:StringLiteralEquality } @Test @@ -131,9 +131,9 @@ public class LinkedHashMapTest { assertNull("Empty hashtable access", empty.get("nothing")); empty.put("something", "here"); - //CHECKSTYLE:OFF + //CHECKSTYLE.OFF:StringLiteralEquality assertTrue("cannot get element", empty.get("something") == "here"); - // CHECKSTYLE: ON + //CHECKSTYLE.ON:StringLiteralEquality } @Test