diff --git a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java index d317557cbb19c..edca90f439695 100644 --- a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java +++ b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java @@ -906,6 +906,31 @@ public AbstractStringBuilder append(char c) { return this; } + /** + * Appends the two-digit string representation of the {@code int} + * argument to this sequence. + *

+ * The integer {@code i} is formatted as two decimal digits. + * If the value is between 0 and 9, it is formatted with a leading zero + * (e.g., 5 becomes "05"). If the value is outside the range 0-99, + * the behavior is unspecified and may result in unexpected output. + * + * @param i an {@code int} (should be between 0 and 99 inclusive). + * @throws IndexOutOfBoundsException if {@code i} is outside the range 0-99. + */ + void appendPair(int i) { + byte coder = this.coder; + int count = this.count; + byte[] value = ensureCapacitySameCoder(this.value, coder, count + 2); + if (isLatin1(coder)) { + DecimalDigits.uncheckedPutPairLatin1(value, count, i); + } else { + DecimalDigits.uncheckedPutPairUTF16(value, count, i); + } + this.value = value; + this.count = count + 2; + } + /** * Appends the string representation of the {@code int} * argument to this sequence. diff --git a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java index a40c27bbf472a..b466ef72df769 100644 --- a/src/java.base/share/classes/java/lang/System.java +++ b/src/java.base/share/classes/java/lang/System.java @@ -2149,6 +2149,10 @@ public byte[] getBytesUTF8NoRepl(String s) { return String.getBytesUTF8NoRepl(s); } + public void appendPair(StringBuilder buf, int v) { + buf.appendPair(v); + } + public void inflateBytesToChars(byte[] src, int srcOff, char[] dst, int dstOff, int len) { StringLatin1.inflate(src, srcOff, dst, dstOff, len); } diff --git a/src/java.base/share/classes/java/time/MonthDay.java b/src/java.base/share/classes/java/time/MonthDay.java index 6244c14e6e126..f6db056101f5d 100644 --- a/src/java.base/share/classes/java/time/MonthDay.java +++ b/src/java.base/share/classes/java/time/MonthDay.java @@ -86,6 +86,8 @@ import java.time.temporal.ValueRange; import java.util.Objects; +import jdk.internal.util.DecimalDigits; + /** * A month-day in the ISO-8601 calendar system, such as {@code --12-03}. *

@@ -748,10 +750,12 @@ public int hashCode() { */ @Override public String toString() { - return new StringBuilder(10).append("--") - .append(month < 10 ? "0" : "").append(month) - .append(day < 10 ? "-0" : "-").append(day) - .toString(); + StringBuilder buf = new StringBuilder(10); + buf.append("--"); + DecimalDigits.appendPair(buf, month); + buf.append('-'); + DecimalDigits.appendPair(buf, day); + return buf.toString(); } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/YearMonth.java b/src/java.base/share/classes/java/time/YearMonth.java index b24151de3f022..65f444d35aba6 100644 --- a/src/java.base/share/classes/java/time/YearMonth.java +++ b/src/java.base/share/classes/java/time/YearMonth.java @@ -99,6 +99,8 @@ import java.time.temporal.ValueRange; import java.util.Objects; +import jdk.internal.util.DecimalDigits; + /** * A year-month in the ISO-8601 calendar system, such as {@code 2007-12}. *

@@ -1206,9 +1208,9 @@ public String toString() { } else { buf.append(year); } - return buf.append(month < 10 ? "-0" : "-") - .append(month) - .toString(); + buf.append('-'); + DecimalDigits.appendPair(buf, month); + return buf.toString(); } //----------------------------------------------------------------------- diff --git a/src/java.base/share/classes/java/time/ZoneOffset.java b/src/java.base/share/classes/java/time/ZoneOffset.java index d93c6e2d46d0b..d98e36914d238 100644 --- a/src/java.base/share/classes/java/time/ZoneOffset.java +++ b/src/java.base/share/classes/java/time/ZoneOffset.java @@ -88,6 +88,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicReferenceArray; +import jdk.internal.util.DecimalDigits; import jdk.internal.vm.annotation.Stable; /** @@ -465,12 +466,14 @@ private static String buildId(int totalSeconds) { StringBuilder buf = new StringBuilder(); int absHours = absTotalSeconds / SECONDS_PER_HOUR; int absMinutes = (absTotalSeconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR; - buf.append(totalSeconds < 0 ? "-" : "+") - .append(absHours < 10 ? "0" : "").append(absHours) - .append(absMinutes < 10 ? ":0" : ":").append(absMinutes); + buf.append(totalSeconds < 0 ? '-' : '+'); + DecimalDigits.appendPair(buf, absHours); + buf.append(':'); + DecimalDigits.appendPair(buf, absMinutes); int absSeconds = absTotalSeconds % SECONDS_PER_MINUTE; if (absSeconds != 0) { - buf.append(absSeconds < 10 ? ":0" : ":").append(absSeconds); + buf.append(':'); + DecimalDigits.appendPair(buf, absSeconds); } return buf.toString(); } diff --git a/src/java.base/share/classes/java/time/chrono/ChronoLocalDateImpl.java b/src/java.base/share/classes/java/time/chrono/ChronoLocalDateImpl.java index ca226b70d24be..67f08c5cb4f73 100644 --- a/src/java.base/share/classes/java/time/chrono/ChronoLocalDateImpl.java +++ b/src/java.base/share/classes/java/time/chrono/ChronoLocalDateImpl.java @@ -74,6 +74,8 @@ import java.time.temporal.ValueRange; import java.util.Objects; +import jdk.internal.util.DecimalDigits; + /** * A date expressed in terms of a standard year-month-day calendar system. *

@@ -426,18 +428,22 @@ public int hashCode() { @Override public String toString() { - // getLong() reduces chances of exceptions in toString() - long yoe = getLong(YEAR_OF_ERA); - long moy = getLong(MONTH_OF_YEAR); - long dom = getLong(DAY_OF_MONTH); + // Using get() instead of getLong() for performance reasons, + // as the values of YEAR_OF_ERA, MONTH_OF_YEAR, and DAY_OF_MONTH + // are guaranteed to be within the int range for all chronologies. + int yoe = get(YEAR_OF_ERA); + int moy = get(MONTH_OF_YEAR); + int dom = get(DAY_OF_MONTH); StringBuilder buf = new StringBuilder(30); buf.append(getChronology().toString()) .append(" ") .append(getEra()) .append(" ") .append(yoe) - .append(moy < 10 ? "-0" : "-").append(moy) - .append(dom < 10 ? "-0" : "-").append(dom); + .append('-'); + DecimalDigits.appendPair(buf, moy); + buf.append('-'); + DecimalDigits.appendPair(buf, dom); return buf.toString(); } diff --git a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java index aa5b6e438f549..1161f85e3795f 100644 --- a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java +++ b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java @@ -386,6 +386,22 @@ public interface JavaLangAccess { */ void uncheckedPutCharUTF16(byte[] bytes, int index, int ch); + /** + * Appends the two-digit string representation of the {@code int} + * argument to the given {@code StringBuilder}. + *

+ * This method is intended for internal use by {@code DecimalDigits.appendPair}. + * The integer {@code v} is formatted as two decimal digits. + * If the value is between 0 and 9, it is formatted with a leading zero + * (e.g., 5 becomes "05"). If the value is outside the range 0-99, + * the behavior is unspecified. + * + * @param buf the {@code StringBuilder} to append to. + * @param v the {@code int} value (should be between 0 and 99 inclusive). + * @see jdk.internal.util.DecimalDigits#appendPair(StringBuilder, int) + */ + void appendPair(StringBuilder buf, int v); + /** * Encode the given string into a sequence of bytes using utf8. * diff --git a/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java b/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java index bbb0b6738d189..356aee739f2d7 100644 --- a/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java +++ b/src/java.base/share/classes/jdk/internal/util/DateTimeHelper.java @@ -49,24 +49,25 @@ public static void formatTo(StringBuilder buf, LocalDateTime dateTime) { * Requires extra capacity of 10 to avoid StringBuilder reallocation. */ public static void formatTo(StringBuilder buf, LocalDate date) { - int year = date.getYear(), - month = date.getMonthValue(), - day = date.getDayOfMonth(); - int absYear = Math.abs(year); + int year = date.getYear(), + absYear = Math.abs(year); if (absYear < 1000) { if (year < 0) { buf.append('-'); } - buf.repeat('0', absYear < 10 ? 3 : absYear < 100 ? 2 : 1); - buf.append(absYear); + int y01 = absYear / 100; + DecimalDigits.appendPair(buf, y01); + DecimalDigits.appendPair(buf, absYear - y01 * 100); } else { if (year > 9999) { buf.append('+'); } buf.append(year); } - buf.append(month < 10 ? "-0" : "-").append(month) - .append(day < 10 ? "-0" : "-").append(day); + buf.append('-'); + DecimalDigits.appendPair(buf, date.getMonthValue()); + buf.append('-'); + DecimalDigits.appendPair(buf, date.getDayOfMonth()); } /** @@ -74,14 +75,14 @@ public static void formatTo(StringBuilder buf, LocalDate date) { * Requires extra capacity of 18 to avoid StringBuilder reallocation. */ public static void formatTo(StringBuilder buf, LocalTime time) { - int hour = time.getHour(), - minute = time.getMinute(), - second = time.getSecond(), + DecimalDigits.appendPair(buf, time.getHour()); + buf.append(':'); + DecimalDigits.appendPair(buf, time.getMinute()); + int second = time.getSecond(), nano = time.getNano(); - buf.append(hour < 10 ? "0" : "").append(hour) - .append(minute < 10 ? ":0" : ":").append(minute); if ((second | nano) > 0) { - buf.append(second < 10 ? ":0" : ":").append(second); + buf.append(':'); + DecimalDigits.appendPair(buf, second); if (nano > 0) { buf.append('.'); int zeros = 9 - DecimalDigits.stringSize(nano); diff --git a/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java b/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java index 6c0c745651e34..785b38ca9207a 100644 --- a/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java +++ b/src/java.base/share/classes/jdk/internal/util/DecimalDigits.java @@ -25,6 +25,7 @@ package jdk.internal.util; +import jdk.internal.access.SharedSecrets; import jdk.internal.misc.Unsafe; import jdk.internal.vm.annotation.Stable; @@ -443,4 +444,22 @@ private static void uncheckedPutCharUTF16(byte[] buf, int charPos, int c) { assert charPos >= 0 && charPos < (buf.length >> 1); UNSAFE.putCharUnaligned(buf, ARRAY_BYTE_BASE_OFFSET + ((long) charPos << 1), (char) c); } + + /** + * Appends the two-digit string representation of the {@code int} + * argument to the given {@code StringBuilder}. + *

+ * The integer {@code v} is formatted as two decimal digits. + * If the value is between 0 and 9, it is formatted with a leading zero + * (e.g., 5 becomes "05"). If the value is outside the range 0-99, + * the behavior is unspecified. + * + * @param buf the {@code StringBuilder} to append to. + * @param v the {@code int} value (should be between 0 and 99 inclusive). + * @see jdk.internal.access.JavaLangAccess#appendPair(StringBuilder, int) + */ + public static void appendPair(StringBuilder buf, int v) { + SharedSecrets.getJavaLangAccess() + .appendPair(buf, v); + } } diff --git a/test/jdk/jdk/internal/util/DecimalDigitsTest.java b/test/jdk/jdk/internal/util/DecimalDigitsTest.java new file mode 100644 index 0000000000000..cdf70514e750d --- /dev/null +++ b/test/jdk/jdk/internal/util/DecimalDigitsTest.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2024, Alibaba Group Holding Limited. 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 test.jdk.internal.util; + +import org.testng.annotations.Test; +import static org.testng.Assert.*; + +import jdk.internal.util.DecimalDigits; + +/* + * @test + * @bug 0000000 + * @summary Test DecimalDigits.appendPair method with LATIN1 and UTF16 encoding + * @modules java.base/jdk.internal.util + * @run testng test.jdk.internal.util.DecimalDigitsTest + */ +public class DecimalDigitsTest { + + @Test + public void testAppendPair() { + // Test values from 0 to 99 + for (int i = 0; i <= 99; i++) { + StringBuilder sb = new StringBuilder(); + DecimalDigits.appendPair(sb, i); + String expected = String.format("%02d", i); + assertEquals(sb.toString(), expected, "Failed for value: " + i); + } + } + + @Test + public void testAppendPairWithLatin1Encoding() { + // Test appendPair with LATIN1 encoding + StringBuilder sb = new StringBuilder(); + + // Force the StringBuilder to use LATIN1 encoding by ensuring all characters are LATIN1 + // This is the default behavior when all characters fit in LATIN1 + for (int i = 0; i <= 99; i++) { + sb.setLength(0); // Clear the StringBuilder + DecimalDigits.appendPair(sb, i); + String expected = String.format("%02d", i); + assertEquals(sb.toString(), expected, "Failed for value: " + i + " with LATIN1 encoding"); + } + } + + @Test + public void testAppendPairWithUTF16Encoding() { + // Test appendPair with UTF16 encoding + StringBuilder sb = new StringBuilder(); + + // Force the StringBuilder to use UTF16 encoding by adding a non-LATIN1 character first + sb.append('€'); // Euro sign is not in LATIN1 + + for (int i = 0; i <= 99; i++) { + int currentLength = sb.length(); + DecimalDigits.appendPair(sb, i); + String expected = String.format("%02d", i); + + // Check that the pair was appended correctly + String appended = sb.substring(currentLength); + assertEquals(appended, expected, "Failed for value: " + i + " with UTF16 encoding"); + } + } +} \ No newline at end of file