diff --git a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java index 700b8f07531..50c4155c4ac 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -701,11 +701,20 @@ public final class DateTimeFormatterBuilder { */ public DateTimeFormatterBuilder appendFraction( TemporalField field, int minWidth, int maxWidth, boolean decimalPoint) { - if (minWidth == maxWidth && decimalPoint == false) { - // adjacent parsing - appendValue(new FractionPrinterParser(field, minWidth, maxWidth, decimalPoint)); + if (field == NANO_OF_SECOND) { + if (minWidth == maxWidth && decimalPoint == false) { + // adjacent parsing + appendValue(new NanosPrinterParser(minWidth, maxWidth, decimalPoint)); + } else { + appendInternal(new NanosPrinterParser(minWidth, maxWidth, decimalPoint)); + } } else { - appendInternal(new FractionPrinterParser(field, minWidth, maxWidth, decimalPoint)); + if (minWidth == maxWidth && decimalPoint == false) { + // adjacent parsing + appendValue(new FractionPrinterParser(field, minWidth, maxWidth, decimalPoint)); + } else { + appendInternal(new FractionPrinterParser(field, minWidth, maxWidth, decimalPoint)); + } } return this; } @@ -2758,6 +2767,24 @@ public final class DateTimeFormatterBuilder { return new NumberPrinterParser(field, minWidth, maxWidth, signStyle, this.subsequentWidth + subsequentWidth); } + /* + * Copied from Long.stringSize + */ + private static int stringSize(long x) { + int d = 1; + if (x >= 0) { + d = 0; + x = -x; + } + long p = -10; + for (int i = 1; i < 19; i++) { + if (x > p) + return i + d; + p = 10 * p; + } + return 19 + d; + } + @Override public boolean format(DateTimePrintContext context, StringBuilder buf) { Long valueLong = context.getValue(field); @@ -2766,18 +2793,21 @@ public final class DateTimeFormatterBuilder { } long value = getValue(context, valueLong); DecimalStyle decimalStyle = context.getDecimalStyle(); - String str = (value == Long.MIN_VALUE ? "9223372036854775808" : Long.toString(Math.abs(value))); - if (str.length() > maxWidth) { + int size = stringSize(value); + if (value < 0) { + size--; + } + + if (size > maxWidth) { throw new DateTimeException("Field " + field + " cannot be printed as the value " + value + " exceeds the maximum print width of " + maxWidth); } - str = decimalStyle.convertNumberToI18N(str); if (value >= 0) { switch (signStyle) { case EXCEEDS_PAD: - if (minWidth < 19 && value >= EXCEED_POINTS[minWidth]) { + if (minWidth < 19 && size > minWidth) { buf.append(decimalStyle.getPositiveSign()); } break; @@ -2793,10 +2823,16 @@ public final class DateTimeFormatterBuilder { " cannot be negative according to the SignStyle"); } } - for (int i = 0; i < minWidth - str.length(); i++) { - buf.append(decimalStyle.getZeroDigit()); + char zeroDigit = decimalStyle.getZeroDigit(); + for (int i = 0; i < minWidth - size; i++) { + buf.append(zeroDigit); + } + if (zeroDigit == '0' && value != Long.MIN_VALUE) { + buf.append(Math.abs(value)); + } else { + String str = value == Long.MIN_VALUE ? "9223372036854775808" : Long.toString(Math.abs(value)); + buf.append(decimalStyle.convertNumberToI18N(str)); } - buf.append(str); return true; } @@ -3111,12 +3147,216 @@ public final class DateTimeFormatterBuilder { } } + //----------------------------------------------------------------------- + /** + * Prints and parses a NANO_OF_SECOND field with optional padding. + */ + static final class NanosPrinterParser extends NumberPrinterParser { + private final boolean decimalPoint; + + /** + * Constructor. + * + * @param minWidth the minimum width to output, from 0 to 9 + * @param maxWidth the maximum width to output, from 0 to 9 + * @param decimalPoint whether to output the localized decimal point symbol + */ + NanosPrinterParser(int minWidth, int maxWidth, boolean decimalPoint) { + this(minWidth, maxWidth, decimalPoint, 0); + if (minWidth < 0 || minWidth > 9) { + throw new IllegalArgumentException("Minimum width must be from 0 to 9 inclusive but was " + minWidth); + } + if (maxWidth < 1 || maxWidth > 9) { + throw new IllegalArgumentException("Maximum width must be from 1 to 9 inclusive but was " + maxWidth); + } + if (maxWidth < minWidth) { + throw new IllegalArgumentException("Maximum width must exceed or equal the minimum width but " + + maxWidth + " < " + minWidth); + } + } + + /** + * Constructor. + * + * @param minWidth the minimum width to output, from 0 to 9 + * @param maxWidth the maximum width to output, from 0 to 9 + * @param decimalPoint whether to output the localized decimal point symbol + * @param subsequentWidth the subsequentWidth for this instance + */ + NanosPrinterParser(int minWidth, int maxWidth, boolean decimalPoint, int subsequentWidth) { + super(NANO_OF_SECOND, minWidth, maxWidth, SignStyle.NOT_NEGATIVE, subsequentWidth); + this.decimalPoint = decimalPoint; + } + + /** + * Returns a new instance with fixed width flag set. + * + * @return a new updated printer-parser, not null + */ + @Override + NanosPrinterParser withFixedWidth() { + if (subsequentWidth == -1) { + return this; + } + return new NanosPrinterParser(minWidth, maxWidth, decimalPoint, -1); + } + + /** + * Returns a new instance with an updated subsequent width. + * + * @param subsequentWidth the width of subsequent non-negative numbers, 0 or greater + * @return a new updated printer-parser, not null + */ + @Override + NanosPrinterParser withSubsequentWidth(int subsequentWidth) { + return new NanosPrinterParser(minWidth, maxWidth, decimalPoint, this.subsequentWidth + subsequentWidth); + } + + /** + * For NanosPrinterParser, the width is fixed if context is strict, + * minWidth equal to maxWidth and decimalpoint is absent. + * @param context the context + * @return if the field is fixed width + * @see #appendFraction(java.time.temporal.TemporalField, int, int, boolean) + */ + @Override + boolean isFixedWidth(DateTimeParseContext context) { + if (context.isStrict() && minWidth == maxWidth && decimalPoint == false) { + return true; + } + return false; + } + + // Simplified variant of Integer.stringSize that assumes positive values + private static int stringSize(int x) { + int p = 10; + for (int i = 1; i < 10; i++) { + if (x < p) + return i; + p = 10 * p; + } + return 10; + } + + private static final int[] TENS = new int[] { + 1, + 10, + 100, + 1000, + 10000, + 100000, + 1000000, + 10000000, + 100000000 + }; + + @Override + public boolean format(DateTimePrintContext context, StringBuilder buf) { + Long value = context.getValue(field); + if (value == null) { + return false; + } + int val = field.range().checkValidIntValue(value, field); + DecimalStyle decimalStyle = context.getDecimalStyle(); + int stringSize = stringSize(val); + char zero = decimalStyle.getZeroDigit(); + if (val == 0 || stringSize < 10 - maxWidth) { + // 0 or would round down to 0 + // to get output identical to FractionPrinterParser use minWidth if the + // value is zero, maxWidth otherwise + int width = val == 0 ? minWidth : maxWidth; + if (width > 0) { + if (decimalPoint) { + buf.append(decimalStyle.getDecimalSeparator()); + } + for (int i = 0; i < width; i++) { + buf.append(zero); + } + } + } else { + if (decimalPoint) { + buf.append(decimalStyle.getDecimalSeparator()); + } + // add leading zeros + for (int i = 9 - stringSize; i > 0; i--) { + buf.append(zero); + } + // truncate unwanted digits + if (maxWidth < 9) { + val /= TENS[9 - maxWidth]; + } + // truncate zeros + for (int i = maxWidth; i > minWidth; i--) { + if ((val % 10) != 0) { + break; + } + val /= 10; + } + if (zero == '0') { + buf.append(val); + } else { + buf.append(decimalStyle.convertNumberToI18N(Integer.toString(val))); + } + } + return true; + } + + @Override + public int parse(DateTimeParseContext context, CharSequence text, int position) { + int effectiveMin = (context.isStrict() || isFixedWidth(context) ? minWidth : 0); + int effectiveMax = (context.isStrict() || isFixedWidth(context) ? maxWidth : 9); + int length = text.length(); + if (position == length) { + // valid if whole field is optional, invalid if minimum width + return (effectiveMin > 0 ? ~position : position); + } + if (decimalPoint) { + if (text.charAt(position) != context.getDecimalStyle().getDecimalSeparator()) { + // valid if whole field is optional, invalid if minimum width + return (effectiveMin > 0 ? ~position : position); + } + position++; + } + int minEndPos = position + effectiveMin; + if (minEndPos > length) { + return ~position; // need at least min width digits + } + int maxEndPos = Math.min(position + effectiveMax, length); + int total = 0; // can use int because we are only parsing up to 9 digits + int pos = position; + while (pos < maxEndPos) { + char ch = text.charAt(pos); + int digit = context.getDecimalStyle().convertToDigit(ch); + if (digit < 0) { + if (pos < minEndPos) { + return ~position; // need at least min width digits + } + break; + } + pos++; + total = total * 10 + digit; + } + for (int i = 9 - (pos - position); i > 0; i--) { + total *= 10; + } + return context.setParsedField(field, total, position, pos); + } + + @Override + public String toString() { + String decimal = (decimalPoint ? ",DecimalPoint" : ""); + return "Fraction(" + field + "," + minWidth + "," + maxWidth + decimal + ")"; + } + } + //----------------------------------------------------------------------- /** * Prints and parses a numeric date-time field with optional padding. */ static final class FractionPrinterParser extends NumberPrinterParser { private final boolean decimalPoint; + private final BigDecimal minBD; + private final BigDecimal rangeBD; /** * Constructor. @@ -3156,6 +3396,9 @@ public final class DateTimeFormatterBuilder { FractionPrinterParser(TemporalField field, int minWidth, int maxWidth, boolean decimalPoint, int subsequentWidth) { super(field, minWidth, maxWidth, SignStyle.NOT_NEGATIVE, subsequentWidth); this.decimalPoint = decimalPoint; + ValueRange range = field.range(); + this.minBD = BigDecimal.valueOf(range.getMinimum()); + this.rangeBD = BigDecimal.valueOf(range.getMaximum()).subtract(minBD).add(BigDecimal.ONE); } /** @@ -3217,12 +3460,12 @@ public final class DateTimeFormatterBuilder { } else { int outputScale = Math.min(Math.max(fraction.scale(), minWidth), maxWidth); fraction = fraction.setScale(outputScale, RoundingMode.FLOOR); - String str = fraction.toPlainString().substring(2); - str = decimalStyle.convertNumberToI18N(str); if (decimalPoint) { buf.append(decimalStyle.getDecimalSeparator()); } - buf.append(str); + String str = fraction.toPlainString(); + str = decimalStyle.convertNumberToI18N(str); + buf.append(str, 2, str.length()); } return true; } @@ -3284,10 +3527,7 @@ public final class DateTimeFormatterBuilder { * @throws DateTimeException if the value cannot be converted to a fraction */ private BigDecimal convertToFraction(long value) { - ValueRange range = field.range(); - range.checkValidValue(value, field); - BigDecimal minBD = BigDecimal.valueOf(range.getMinimum()); - BigDecimal rangeBD = BigDecimal.valueOf(range.getMaximum()).subtract(minBD).add(BigDecimal.ONE); + field.range().checkValidValue(value, field); BigDecimal valueBD = BigDecimal.valueOf(value).subtract(minBD); BigDecimal fraction = valueBD.divide(rangeBD, 9, RoundingMode.FLOOR); // stripTrailingZeros bug @@ -3311,9 +3551,6 @@ public final class DateTimeFormatterBuilder { * @throws DateTimeException if the value cannot be converted */ private long convertFromFraction(BigDecimal fraction) { - ValueRange range = field.range(); - BigDecimal minBD = BigDecimal.valueOf(range.getMinimum()); - BigDecimal rangeBD = BigDecimal.valueOf(range.getMaximum()).subtract(minBD).add(BigDecimal.ONE); BigDecimal valueBD = fraction.multiply(rangeBD).setScale(0, RoundingMode.FLOOR).add(minBD); return valueBD.longValueExact(); } diff --git a/src/java.base/share/classes/java/time/format/DateTimePrintContext.java b/src/java.base/share/classes/java/time/format/DateTimePrintContext.java index 0faec561724..c7eed23b7bc 100644 --- a/src/java.base/share/classes/java/time/format/DateTimePrintContext.java +++ b/src/java.base/share/classes/java/time/format/DateTimePrintContext.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2016, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -98,11 +98,11 @@ final class DateTimePrintContext { /** * The temporal being output. */ - private TemporalAccessor temporal; + private final TemporalAccessor temporal; /** * The formatter, not null. */ - private DateTimeFormatter formatter; + private final DateTimeFormatter formatter; /** * Whether the current formatter is optional. */ diff --git a/test/jdk/java/time/test/java/time/format/TestFractionPrinterParser.java b/test/jdk/java/time/test/java/time/format/TestFractionPrinterParser.java index d95591ac520..4dbca06709d 100644 --- a/test/jdk/java/time/test/java/time/format/TestFractionPrinterParser.java +++ b/test/jdk/java/time/test/java/time/format/TestFractionPrinterParser.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2019, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -59,9 +59,7 @@ */ package test.java.time.format; -import static java.time.temporal.ChronoField.MILLI_OF_SECOND; -import static java.time.temporal.ChronoField.NANO_OF_SECOND; -import static java.time.temporal.ChronoField.SECOND_OF_MINUTE; +import static java.time.temporal.ChronoField.*; import static org.testng.Assert.assertEquals; import static org.testng.Assert.fail; @@ -69,6 +67,7 @@ import java.text.ParsePosition; import java.time.DateTimeException; import java.time.LocalTime; import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalField; @@ -77,9 +76,9 @@ import org.testng.annotations.Test; import test.java.time.temporal.MockFieldValue; /** - * Test FractionPrinterParser. + * Test formatters obtained with DateTimeFormatterBuilder.appendFraction (FractionPrinterParser, NanosPrinterParser) * - * @bug 8230136 + * @bug 8230136 8276220 */ @Test public class TestFractionPrinterParser extends AbstractTestPrinterParser { @@ -96,12 +95,54 @@ public class TestFractionPrinterParser extends AbstractTestPrinterParser { getFormatter(NANO_OF_SECOND, 0, 9, true).formatTo(EMPTY_DTA, buf); } + @DataProvider(name="OOB_Nanos") + Object[][] provider_oob_nanos() { + return new Object[][]{ + {-1}, + {1_000_000_000}, + {Integer.MIN_VALUE}, + {Integer.MAX_VALUE}, + {Integer.MAX_VALUE + 1L}, + {Long.MAX_VALUE}, + {Long.MIN_VALUE}, + }; + } + + @Test(dataProvider="OOB_Nanos", expectedExceptions=DateTimeException.class) + public void test_print_oob_nanos(long value) throws Exception { + getFormatter(NANO_OF_SECOND, 0, 9, true).formatTo(new MockFieldValue(NANO_OF_SECOND, value), buf); + } + + @DataProvider(name="OOB_Micros") + Object[][] provider_oob_micros() { + return new Object[][]{ + {-1}, + {1_000_000}, + {Integer.MIN_VALUE}, + {Integer.MAX_VALUE}, + {Integer.MAX_VALUE + 1L}, + {Long.MAX_VALUE}, + {Long.MIN_VALUE}, + }; + } + + @Test(dataProvider="OOB_Micros", expectedExceptions=DateTimeException.class) + public void test_print_oob_micros(long value) throws Exception { + getFormatter(MICRO_OF_SECOND, 0, 9, true).formatTo(new MockFieldValue(MICRO_OF_SECOND, value), buf); + } + public void test_print_append() throws Exception { buf.append("EXISTING"); getFormatter(NANO_OF_SECOND, 0, 9, true).formatTo(LocalTime.of(12, 30, 40, 3), buf); assertEquals(buf.toString(), "EXISTING.000000003"); } + public void test_print_append_micros() throws Exception { + buf.append("EXISTING"); + getFormatter(MICRO_OF_SECOND, 0, 6, true).formatTo(LocalTime.of(12, 30, 40, 3000), buf); + assertEquals(buf.toString(), "EXISTING.000003"); + } + //----------------------------------------------------------------------- @DataProvider(name="Nanos") Object[][] provider_nanos() { @@ -125,6 +166,31 @@ public class TestFractionPrinterParser extends AbstractTestPrinterParser { {0, 9, 1234567, ".001234567"}, {0, 9, 12345678, ".012345678"}, {0, 9, 123456789, ".123456789"}, + {0, 9, 9, ".000000009"}, + {0, 9, 99, ".000000099"}, + {0, 9, 999, ".000000999"}, + {0, 9, 9999, ".000009999"}, + {0, 9, 99999, ".000099999"}, + {0, 9, 999999, ".000999999"}, + {0, 9, 9999999, ".009999999"}, + {0, 9, 99999999, ".099999999"}, + {0, 9, 999999999, ".999999999"}, + {0, 8, 9, ".00000000"}, + {0, 7, 99, ".0000000"}, + {0, 6, 999, ".000000"}, + {0, 5, 9999, ".00000"}, + {0, 4, 99999, ".0000"}, + {0, 3, 999999, ".000"}, + {0, 2, 9999999, ".00"}, + {0, 1, 99999999, ".0"}, + {0, 8, 1, ".00000000"}, + {0, 7, 11, ".0000000"}, + {0, 6, 111, ".000000"}, + {0, 5, 1111, ".00000"}, + {0, 4, 11111, ".0000"}, + {0, 3, 111111, ".000"}, + {0, 2, 1111111, ".00"}, + {0, 1, 11111111, ".0"}, {1, 9, 0, ".0"}, {1, 9, 2, ".000000002"}, @@ -136,6 +202,24 @@ public class TestFractionPrinterParser extends AbstractTestPrinterParser { {1, 9, 2000000, ".002"}, {1, 9, 20000000, ".02"}, {1, 9, 200000000, ".2"}, + {1, 9, 1, ".000000001"}, + {1, 9, 10, ".00000001"}, + {1, 9, 100, ".0000001"}, + {1, 9, 1000, ".000001"}, + {1, 9, 10000, ".00001"}, + {1, 9, 100000, ".0001"}, + {1, 9, 1000000, ".001"}, + {1, 9, 10000000, ".01"}, + {1, 9, 100000000, ".1"}, + {1, 9, 9, ".000000009"}, + {1, 9, 99, ".000000099"}, + {1, 9, 999, ".000000999"}, + {1, 9, 9999, ".000009999"}, + {1, 9, 99999, ".000099999"}, + {1, 9, 999999, ".000999999"}, + {1, 9, 9999999, ".009999999"}, + {1, 9, 99999999, ".099999999"}, + {1, 9, 999999999, ".999999999"}, {2, 3, 0, ".00"}, {2, 3, 2, ".000"}, @@ -176,6 +260,9 @@ public class TestFractionPrinterParser extends AbstractTestPrinterParser { {6, 6, 1234567, ".001234"}, {6, 6, 12345678, ".012345"}, {6, 6, 123456789, ".123456"}, + {7, 7, 123456789, ".1234567"}, + {8, 8, 123456789, ".12345678"}, + {9, 9, 123456789, ".123456789"}, }; } @@ -197,6 +284,82 @@ public class TestFractionPrinterParser extends AbstractTestPrinterParser { assertEquals(buf.toString(), (result.startsWith(".") ? result.substring(1) : result)); } + //----------------------------------------------------------------------- + @DataProvider(name="Micros") + Object[][] provider_micros() { + return new Object[][] { + {0, 6, 0, ""}, + {0, 6, 2, ".000002"}, + {0, 6, 20, ".00002"}, + {0, 6, 200, ".0002"}, + {0, 6, 2000, ".002"}, + {0, 6, 20000, ".02"}, + {0, 6, 200000, ".2"}, + {0, 6, 1, ".000001"}, + {0, 6, 12, ".000012"}, + {0, 6, 123, ".000123"}, + {0, 6, 1234, ".001234"}, + {0, 6, 12345, ".012345"}, + {0, 6, 123456, ".123456"}, + {0, 6, 9, ".000009"}, + {0, 6, 99, ".000099"}, + {0, 6, 999, ".000999"}, + {0, 6, 9999, ".009999"}, + {0, 6, 99999, ".099999"}, + {0, 6, 999999, ".999999"}, + {0, 5, 9, ".00000"}, + {0, 4, 99, ".0000"}, + {0, 3, 999, ".000"}, + {0, 2, 9999, ".00"}, + {0, 1, 99999, ".0"}, + {0, 5, 1, ".00000"}, + {0, 4, 11, ".0000"}, + {0, 3, 111, ".000"}, + {0, 2, 1111, ".00"}, + {0, 1, 11111, ".0"}, + + {1, 6, 0, ".0"}, + {1, 6, 2, ".000002"}, + {1, 6, 20, ".00002"}, + {1, 6, 200, ".0002"}, + {1, 6, 2000, ".002"}, + {1, 6, 20000, ".02"}, + {1, 6, 200000, ".2"}, + + {2, 3, 0, ".00"}, + {2, 3, 2, ".000"}, + {2, 3, 20, ".000"}, + {2, 3, 200, ".000"}, + {2, 3, 2000, ".002"}, + {2, 3, 20000, ".02"}, + {2, 3, 200000, ".20"}, + {2, 3, 1, ".000"}, + {2, 3, 12, ".000"}, + {2, 3, 123, ".000"}, + {2, 3, 1234, ".001"}, + {2, 3, 12345, ".012"}, + {2, 3, 123456, ".123"}, + }; + } + + @Test(dataProvider="Micros") + public void test_print_micros(int minWidth, int maxWidth, int value, String result) throws Exception { + getFormatter(MICRO_OF_SECOND, minWidth, maxWidth, true).formatTo(new MockFieldValue(MICRO_OF_SECOND, value), buf); + if (result == null) { + fail("Expected exception"); + } + assertEquals(buf.toString(), result); + } + + @Test(dataProvider="Micros") + public void test_print_micros_noDecimalPoint(int minWidth, int maxWidth, int value, String result) throws Exception { + getFormatter(MICRO_OF_SECOND, minWidth, maxWidth, false).formatTo(new MockFieldValue(MICRO_OF_SECOND, value), buf); + if (result == null) { + fail("Expected exception"); + } + assertEquals(buf.toString(), (result.startsWith(".") ? result.substring(1) : result)); + } + //----------------------------------------------------------------------- @DataProvider(name="Seconds") Object[][] provider_seconds() { diff --git a/test/micro/org/openjdk/bench/java/time/format/DateTimeFormatterBench.java b/test/micro/org/openjdk/bench/java/time/format/DateTimeFormatterBench.java new file mode 100644 index 00000000000..6900ee32ee6 --- /dev/null +++ b/test/micro/org/openjdk/bench/java/time/format/DateTimeFormatterBench.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ +package org.openjdk.bench.java.time.format; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Warmup(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Measurement(iterations = 5, time = 5, timeUnit = TimeUnit.SECONDS) +@Fork(3) +@State(Scope.Benchmark) +public class DateTimeFormatterBench { + + private static final TimeZone TIME_ZONE = TimeZone.getTimeZone("UTC"); + + private static final Instant[] INSTANTS = createInstants(); + + private static final ZonedDateTime[] ZONED_DATE_TIMES = createZonedDateTimes(); + + @Param({ + "HH:mm:ss", + "HH:mm:ss.SSS", + "yyyy-MM-dd'T'HH:mm:ss", + "yyyy-MM-dd'T'HH:mm:ss.SSS" + }) + public String pattern; + + private static Instant[] createInstants() { + // Various instants during the same day + final Instant loInstant = Instant.EPOCH.plus(Duration.ofDays(365*50)); // 2020-01-01 + final Instant hiInstant = loInstant.plus(Duration.ofDays(1)); + final long maxOffsetNanos = Duration.between(loInstant, hiInstant).toNanos(); + final Random random = new Random(0); + return IntStream + .range(0, 1_000) + .mapToObj(ignored -> { + final long offsetNanos = (long) Math.floor(random.nextDouble() * maxOffsetNanos); + return loInstant.plus(offsetNanos, ChronoUnit.NANOS); + }) + .toArray(Instant[]::new); + } + + private static ZonedDateTime[] createZonedDateTimes() { + return Stream.of(INSTANTS) + .map(instant -> ZonedDateTime.ofInstant(instant, TIME_ZONE.toZoneId())) + .toArray(ZonedDateTime[]::new); + } + + private StringBuilder stringBuilder = new StringBuilder(100); + private DateTimeFormatter dateTimeFormatter; + + @Setup + public void setup() { + dateTimeFormatter = DateTimeFormatter + .ofPattern(pattern, Locale.US) + .withZone(TIME_ZONE.toZoneId()); + } + + @Benchmark + public void formatInstants(final Blackhole blackhole) { + for (final Instant instant : INSTANTS) { + stringBuilder.setLength(0); + dateTimeFormatter.formatTo(instant, stringBuilder); + blackhole.consume(stringBuilder); + } + } + + @Benchmark + public void formatZonedDateTime(final Blackhole blackhole) { + for (final ZonedDateTime zonedDateTime : ZONED_DATE_TIMES) { + stringBuilder.setLength(0); + dateTimeFormatter.formatTo(zonedDateTime, stringBuilder); + blackhole.consume(stringBuilder); + } + } +}