8366224: Introduce DecimalDigits.appendPair for efficient two-digit formatting and refactor DateTimeHelper

Reviewed-by: liach, rriggs
This commit is contained in:
Shaojin Wen 2025-11-26 05:46:06 +00:00
parent 65f1ad6169
commit 4ffdf7af88
6 changed files with 104 additions and 36 deletions

View File

@ -88,6 +88,8 @@ import java.time.temporal.UnsupportedTemporalTypeException;
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}.
* <p>
@ -764,10 +766,12 @@ public final class MonthDay
*/
@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();
}
//-----------------------------------------------------------------------

View File

@ -101,6 +101,8 @@ import java.time.temporal.UnsupportedTemporalTypeException;
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}.
* <p>
@ -1213,18 +1215,17 @@ public final class YearMonth
public String toString() {
int absYear = Math.abs(year);
StringBuilder buf = new StringBuilder(9);
if (absYear < 1000) {
if (absYear < 10000) {
if (year < 0) {
buf.append(year - 10000).deleteCharAt(1);
} else {
buf.append(year + 10000).deleteCharAt(0);
buf.append('-');
}
DecimalDigits.appendQuad(buf, absYear);
} else {
buf.append(year);
}
return buf.append(month < 10 ? "-0" : "-")
.append(month)
.toString();
buf.append('-');
DecimalDigits.appendPair(buf, month);
return buf.toString();
}
//-----------------------------------------------------------------------

View File

@ -88,6 +88,7 @@ import java.util.concurrent.ConcurrentHashMap;
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 @@ public final class ZoneOffset
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();
}

View File

@ -74,6 +74,8 @@ import java.time.temporal.UnsupportedTemporalTypeException;
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.
* <p>
@ -426,18 +428,22 @@ abstract class ChronoLocalDateImpl<D extends ChronoLocalDate>
@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();
}

View File

@ -49,24 +49,23 @@ public final class DateTimeHelper {
* 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);
if (absYear < 1000) {
int year = date.getYear(),
absYear = Math.abs(year);
if (absYear < 10000) {
if (year < 0) {
buf.append('-');
}
buf.repeat('0', absYear < 10 ? 3 : absYear < 100 ? 2 : 1);
buf.append(absYear);
DecimalDigits.appendQuad(buf, absYear);
} 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 +73,14 @@ public final class DateTimeHelper {
* 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);

View File

@ -25,6 +25,8 @@
package jdk.internal.util;
import jdk.internal.access.JavaLangAccess;
import jdk.internal.access.SharedSecrets;
import jdk.internal.misc.Unsafe;
import jdk.internal.vm.annotation.Stable;
@ -36,6 +38,7 @@ import static jdk.internal.misc.Unsafe.ARRAY_BYTE_BASE_OFFSET;
* @since 21
*/
public final class DecimalDigits {
private static final JavaLangAccess JLA = SharedSecrets.getJavaLangAccess();
private static final Unsafe UNSAFE = Unsafe.getUnsafe();
/**
@ -443,4 +446,56 @@ public final class DecimalDigits {
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}.
* <p>
* The integer {@code v} is formatted as two decimal digits.
* Values from 0 to 9 are formatted with a leading zero (e.g., 5 becomes "05"),
* and values from 10 to 99 are formatted as regular two-digit numbers.
* 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).
*/
public static void appendPair(StringBuilder buf, int v) {
// The & 0x7f operation keeps the index within the safe range [0, 127] for the DIGITS array,
// which allows the JIT compiler to eliminate array bounds checks for performance.
int packed = DIGITS[v & 0x7f];
// The temporary String and byte[] objects created here are typically eliminated
// by the JVM's escape analysis and scalar replacement optimizations during
// runtime compilation, avoiding actual heap allocations in optimized code.
buf.append(
JLA.uncheckedNewStringWithLatin1Bytes(
new byte[] {(byte) packed, (byte) (packed >> 8)}));
}
/**
* Appends the four-digit string representation of the {@code int}
* argument to the given {@code StringBuilder}.
* <p>
* The integer {@code v} is formatted as four decimal digits.
* Values from 0 to 9 are formatted with leading zeros (e.g., 5 becomes "0005"),
* values from 10 to 99 add two leading zeros (e.g., 25 becomes "0025"),
* values from 100 to 999 add one leading zero (e.g., 123 becomes "0123"),
* and values from 1000 to 9999 have no leading zeros.
* If the value is outside the range 0-9999, the behavior is unspecified.
*
* @param buf the {@code StringBuilder} to append to.
* @param v the {@code int} value (should be between 0 and 9999 inclusive).
*/
public static void appendQuad(StringBuilder buf, int v) {
// The & 0x7f operation keeps the index within the safe range [0, 127] for the DIGITS array,
// which allows the JIT compiler to eliminate array bounds checks for performance.
int packedHigh = DIGITS[(v / 100) & 0x7f];
int packedLow = DIGITS[(v % 100) & 0x7f];
// The temporary String and byte[] objects created here are typically eliminated
// by the JVM's escape analysis and scalar replacement optimizations during
// runtime compilation, avoiding actual heap allocations in optimized code.
buf.append(
JLA.uncheckedNewStringWithLatin1Bytes(
new byte[] {(byte) packedHigh, (byte) (packedHigh >> 8),
(byte) packedLow, (byte) (packedLow >> 8)}));
}
}