diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
index 210689fe356..7829d234aaa 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatterTest.java
@@ -32,9 +32,9 @@
import java.util.stream.IntStream;
import java.util.stream.Stream;
import org.apache.logging.log4j.core.time.MutableInstant;
-import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.CompositePatternSequence;
import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.DynamicPatternSequence;
import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.PatternSequence;
+import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.SecondPatternSequence;
import org.apache.logging.log4j.core.util.internal.instant.InstantPatternDynamicFormatter.StaticPatternSequence;
import org.apache.logging.log4j.util.Constants;
import org.junit.jupiter.params.ParameterizedTest;
@@ -55,26 +55,23 @@ void sequencing_should_work(
static List sequencingTestCases() {
final List testCases = new ArrayList<>();
+ // Merged constants
+ testCases.add(Arguments.of(":'foo',", ChronoUnit.DAYS, singletonList(new StaticPatternSequence(":foo,"))));
+
// `SSSX` should be treated constant for daily updates
- testCases.add(Arguments.of("SSSX", ChronoUnit.DAYS, singletonList(pCom(pDyn("SSS"), pDyn("X")))));
+ testCases.add(Arguments.of("SSSX", ChronoUnit.DAYS, asList(pMilliSec(), pDyn("X"))));
// `yyyyMMddHHmmssSSSX` instant cache updated hourly
testCases.add(Arguments.of(
"yyyyMMddHHmmssSSSX",
ChronoUnit.HOURS,
- asList(
- pCom(pDyn("yyyy"), pDyn("MM"), pDyn("dd"), pDyn("HH")),
- pCom(pDyn("mm"), pDyn("ss"), pDyn("SSS")),
- pDyn("X"))));
+ asList(pDyn("yyyyMMddHH", ChronoUnit.HOURS), pDyn("mm"), pSec("", 3), pDyn("X"))));
// `yyyyMMddHHmmssSSSX` instant cache updated per minute
testCases.add(Arguments.of(
"yyyyMMddHHmmssSSSX",
ChronoUnit.MINUTES,
- asList(
- pCom(pDyn("yyyy"), pDyn("MM"), pDyn("dd"), pDyn("HH"), pDyn("mm")),
- pCom(pDyn("ss"), pDyn("SSS")),
- pDyn("X"))));
+ asList(pDyn("yyyyMMddHHmm", ChronoUnit.MINUTES), pSec("", 3), pDyn("X"))));
// ISO9601 instant cache updated daily
final String iso8601InstantPattern = "yyyy-MM-dd'T'HH:mm:ss.SSSX";
@@ -82,77 +79,50 @@ static List sequencingTestCases() {
iso8601InstantPattern,
ChronoUnit.DAYS,
asList(
- pCom(pDyn("yyyy"), pSta("-"), pDyn("MM"), pSta("-"), pDyn("dd"), pSta("T")),
- pCom(
- pDyn("HH"),
- pSta(":"),
- pDyn("mm"),
- pSta(":"),
- pDyn("ss"),
- pSta("."),
- pDyn("SSS"),
- pDyn("X")))));
+ pDyn("yyyy'-'MM'-'dd'T'", ChronoUnit.DAYS),
+ pDyn("HH':'mm':'", ChronoUnit.MINUTES),
+ pSec(".", 3),
+ pDyn("X"))));
// ISO9601 instant cache updated per minute
testCases.add(Arguments.of(
iso8601InstantPattern,
ChronoUnit.MINUTES,
- asList(
- pCom(
- pDyn("yyyy"),
- pSta("-"),
- pDyn("MM"),
- pSta("-"),
- pDyn("dd"),
- pSta("T"),
- pDyn("HH"),
- pSta(":"),
- pDyn("mm"),
- pSta(":")),
- pCom(pDyn("ss"), pSta("."), pDyn("SSS")),
- pDyn("X"))));
+ asList(pDyn("yyyy'-'MM'-'dd'T'HH':'mm':'", ChronoUnit.MINUTES), pSec(".", 3), pDyn("X"))));
// ISO9601 instant cache updated per second
testCases.add(Arguments.of(
iso8601InstantPattern,
ChronoUnit.SECONDS,
- asList(
- pCom(
- pDyn("yyyy"),
- pSta("-"),
- pDyn("MM"),
- pSta("-"),
- pDyn("dd"),
- pSta("T"),
- pDyn("HH"),
- pSta(":"),
- pDyn("mm"),
- pSta(":"),
- pDyn("ss"),
- pSta(".")),
- pDyn("SSS"),
- pDyn("X"))));
+ asList(pDyn("yyyy'-'MM'-'dd'T'HH':'mm':'", ChronoUnit.MINUTES), pSec(".", 3), pDyn("X"))));
+
+ // Seconds and micros
+ testCases.add(Arguments.of(
+ "HH:mm:ss.SSSSSS", ChronoUnit.MINUTES, asList(pDyn("HH':'mm':'", ChronoUnit.MINUTES), pSec(".", 6))));
return testCases;
}
- private static CompositePatternSequence pCom(final PatternSequence... sequences) {
- return new CompositePatternSequence(asList(sequences));
+ private static DynamicPatternSequence pDyn(final String singlePattern) {
+ return new DynamicPatternSequence(singlePattern);
+ }
+
+ private static DynamicPatternSequence pDyn(final String pattern, final ChronoUnit precision) {
+ return new DynamicPatternSequence(pattern, precision);
}
- private static DynamicPatternSequence pDyn(final String pattern) {
- return new DynamicPatternSequence(pattern);
+ private static SecondPatternSequence pSec(String separator, int fractionalDigits) {
+ return new SecondPatternSequence(true, separator, fractionalDigits);
}
- private static StaticPatternSequence pSta(final String literal) {
- return new StaticPatternSequence(literal);
+ private static SecondPatternSequence pMilliSec() {
+ return new SecondPatternSequence(false, "", 3);
}
@ParameterizedTest
@ValueSource(
strings = {
// Basics
- "S",
"SSSSSSS",
"SSSSSSSSS",
"n",
@@ -163,8 +133,7 @@ private static StaticPatternSequence pSta(final String literal) {
"yyyy-MM-dd HH:mm:ss,SSSSSSS",
"yyyy-MM-dd HH:mm:ss,SSSSSSSS",
"yyyy-MM-dd HH:mm:ss,SSSSSSSSS",
- "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS",
- "yyyy-MM-dd'T'HH:mm:ss.SXXX"
+ "yyyy-MM-dd'T'HH:mm:ss.SSSSSSSSS"
})
void should_recognize_patterns_of_nano_precision(final String pattern) {
assertPatternPrecision(pattern, ChronoUnit.NANOS);
@@ -233,20 +202,31 @@ void should_recognize_patterns_of_second_precision(final String pattern) {
assertPatternPrecision(pattern, ChronoUnit.SECONDS);
}
- @ParameterizedTest
- @ValueSource(
- strings = {
+ static Stream should_recognize_patterns_of_minute_precision() {
+ Stream stream = Stream.of(
// Basics
"m",
"mm",
+ "Z",
+ "x",
+ "X",
+ "O",
+ "z",
+ "VV",
// Mixed with other stuff
"yyyy-MM-dd HH:mm",
"yyyy-MM-dd'T'HH:mm",
"HH:mm",
+ "yyyy-MM-dd HH x",
+ "yyyy-MM-dd'T'HH XX",
// Single-quoted text containing nanosecond and millisecond directives
"yyyy-MM-dd'S'HH:mm",
- "yyyy-MM-dd'n'HH:mm"
- })
+ "yyyy-MM-dd'n'HH:mm");
+ return Constants.JAVA_MAJOR_VERSION > 8 ? Stream.concat(stream, Stream.of("v")) : stream;
+ }
+
+ @ParameterizedTest
+ @MethodSource
void should_recognize_patterns_of_minute_precision(final String pattern) {
assertPatternPrecision(pattern, ChronoUnit.MINUTES);
}
@@ -267,28 +247,71 @@ static List hourPrecisionPatterns() {
"K",
"k",
"H",
- "Z",
- "x",
- "X",
- "O",
- "z",
- "VV",
// Mixed with other stuff
"yyyy-MM-dd HH",
"yyyy-MM-dd'T'HH",
- "yyyy-MM-dd HH x",
- "yyyy-MM-dd'T'HH XX",
"ddHH",
// Single-quoted text containing nanosecond and millisecond directives
"yyyy-MM-dd'S'HH",
"yyyy-MM-dd'n'HH"));
if (Constants.JAVA_MAJOR_VERSION > 8) {
java8Patterns.add("B");
- java8Patterns.add("v");
}
return java8Patterns;
}
+ static Stream dynamic_pattern_should_correctly_determine_precision() {
+ // When no a precise unit is not available, uses the closest smaller unit.
+ return Stream.of(
+ Arguments.of("G", ChronoUnit.ERAS),
+ Arguments.of("u", ChronoUnit.YEARS),
+ Arguments.of("D", ChronoUnit.DAYS),
+ Arguments.of("M", ChronoUnit.MONTHS),
+ Arguments.of("L", ChronoUnit.MONTHS),
+ Arguments.of("d", ChronoUnit.DAYS),
+ Arguments.of("Q", ChronoUnit.MONTHS),
+ Arguments.of("q", ChronoUnit.MONTHS),
+ Arguments.of("Y", ChronoUnit.YEARS),
+ Arguments.of("w", ChronoUnit.WEEKS),
+ Arguments.of("W", ChronoUnit.DAYS), // The month can change in the middle of the week
+ Arguments.of("F", ChronoUnit.DAYS), // The month can change in the middle of the week
+ Arguments.of("E", ChronoUnit.DAYS),
+ Arguments.of("e", ChronoUnit.DAYS),
+ Arguments.of("c", ChronoUnit.DAYS),
+ Arguments.of("a", ChronoUnit.HOURS), // Let us round it down
+ Arguments.of("h", ChronoUnit.HOURS),
+ Arguments.of("K", ChronoUnit.HOURS),
+ Arguments.of("k", ChronoUnit.HOURS),
+ Arguments.of("H", ChronoUnit.HOURS),
+ Arguments.of("m", ChronoUnit.MINUTES),
+ Arguments.of("s", ChronoUnit.SECONDS),
+ Arguments.of("S", ChronoUnit.MILLIS),
+ Arguments.of("SS", ChronoUnit.MILLIS),
+ Arguments.of("SSS", ChronoUnit.MILLIS),
+ Arguments.of("SSSS", ChronoUnit.MICROS),
+ Arguments.of("SSSSS", ChronoUnit.MICROS),
+ Arguments.of("SSSSSS", ChronoUnit.MICROS),
+ Arguments.of("SSSSSSS", ChronoUnit.NANOS),
+ Arguments.of("SSSSSSSS", ChronoUnit.NANOS),
+ Arguments.of("SSSSSSSSS", ChronoUnit.NANOS),
+ Arguments.of("A", ChronoUnit.MILLIS),
+ Arguments.of("n", ChronoUnit.NANOS),
+ Arguments.of("N", ChronoUnit.NANOS),
+ // Time zones can change in the middle of a UTC hour (e.g. India)
+ Arguments.of("VV", ChronoUnit.MINUTES),
+ Arguments.of("z", ChronoUnit.MINUTES),
+ Arguments.of("O", ChronoUnit.MINUTES),
+ Arguments.of("X", ChronoUnit.MINUTES),
+ Arguments.of("x", ChronoUnit.MINUTES),
+ Arguments.of("Z", ChronoUnit.MINUTES));
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void dynamic_pattern_should_correctly_determine_precision(String singlePattern, ChronoUnit expectedPrecision) {
+ assertThat(pDyn(singlePattern).precision).isEqualTo(expectedPrecision);
+ }
+
private static void assertPatternPrecision(final String pattern, final ChronoUnit expectedPrecision) {
final InstantPatternFormatter formatter =
new InstantPatternDynamicFormatter(pattern, Locale.getDefault(), TimeZone.getDefault());
@@ -351,7 +374,11 @@ static Stream formatterInputs(final String pattern) {
private static MutableInstant randomInstant() {
final MutableInstant instant = new MutableInstant();
- final long epochSecond = RANDOM.nextInt(1_621_280_470); // 2021-05-17 21:41:10
+ // In the 1970's some time zones had sub-minute offsets to UTC, e.g., Africa/Monrovia.
+ // We will exclude them for tests:
+ final int minEpochSecond = 315_532_800; // 1980-01-01 01:00:00
+ final int maxEpochSecond = 1_621_280_470; // 2021-05-17 21:41:10
+ final long epochSecond = minEpochSecond + RANDOM.nextInt(maxEpochSecond - minEpochSecond);
final int epochSecondNano = randomNanos();
instant.initFromEpochSecond(epochSecond, epochSecondNano);
return instant;
diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
index b25fb85d741..1f94b0b9aef 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternThreadLocalCachedFormatterTest.java
@@ -107,7 +107,7 @@ static Object[][] getterTestCases() {
}
@ParameterizedTest
- @ValueSource(strings = {"S", "SSSS", "SSSSS", "SSSSSS", "SSSSSSS", "SSSSSSSS", "SSSSSSSSS", "n", "N"})
+ @ValueSource(strings = {"SSSS", "SSSSS", "SSSSSS", "SSSSSSS", "SSSSSSSS", "SSSSSSSSS", "n", "N"})
void ofMilliPrecision_should_fail_on_inconsistent_precision(final String subMilliPattern) {
final InstantPatternDynamicFormatter dynamicFormatter =
new InstantPatternDynamicFormatter(subMilliPattern, LOCALE, TIME_ZONE);
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
index 9c93dd34066..bb8059329ea 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/util/internal/instant/InstantPatternDynamicFormatter.java
@@ -29,10 +29,11 @@
import java.util.Objects;
import java.util.TimeZone;
import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
+import java.util.stream.Stream;
import org.apache.logging.log4j.core.time.Instant;
import org.apache.logging.log4j.core.time.MutableInstant;
+import org.apache.logging.log4j.util.BiConsumer;
+import org.apache.logging.log4j.util.Strings;
import org.jspecify.annotations.Nullable;
/**
@@ -47,30 +48,6 @@
* Precompute and cache the output for parts that are of precision lower than or equal to {@value InstantPatternDynamicFormatter#PRECISION_THRESHOLD} (i.e., {@code yyyy-MM-dd'T'HH:mm:} and {@code X}) and cache it
* Upon a formatting request, combine the cached outputs with the dynamic parts (i.e., {@code ss.SSS})
*
- * Implementation note
- *
- * Formatting can actually even be made faster and garbage-free by manually formatting sub-minute precision directives as follows:
- *
- * {@code
- * int offsetMillis = timeZone.getOffset(mutableInstant.getEpochMillisecond());
- * long adjustedEpochSeconds = (instant.getEpochMillisecond() + offsetMillis) / 1000;
- * int local_s = (int) (adjustedEpochSeconds % 60);
- * int local_S = instant.getNanoOfSecond() / 100000000;
- * int local_SS = instant.getNanoOfSecond() / 10000000;
- * int local_SSS = instant.getNanoOfSecond() / 1000000;
- * int local_SSSS = instant.getNanoOfSecond() / 100000;
- * int local_SSSSS = instant.getNanoOfSecond() / 10000;
- * int local_SSSSSS = instant.getNanoOfSecond() / 1000;
- * int local_SSSSSSS = instant.getNanoOfSecond() / 100;
- * int local_SSSSSSSS = instant.getNanoOfSecond() / 10;
- * int local_SSSSSSSSS = instant.getNanoOfSecond();
- * int local_n = instant.getNanoOfSecond();
- * }
- *
- * Though this will require more hardcoded formatting and a change in the sequence merging strategies.
- * Hence, this optimization is intentionally shelved off due to involved complexity.
- * See {@code verify_manually_computed_sub_minute_precision_values()} in {@code InstantPatternDynamicFormatterTest} for a demonstration of this optimization.
- *
*
* @since 2.25.0
*/
@@ -165,7 +142,7 @@ private static InstantPatternFormatter createFormatter(
// Sequence the pattern and create associated formatters
final List sequences = sequencePattern(pattern, precisionThreshold);
- final List formatters = sequences.stream()
+ final InstantPatternFormatter[] formatters = sequences.stream()
.map(sequence -> {
final InstantPatternFormatter formatter = sequence.createFormatter(locale, timeZone);
final boolean constant = sequence.isConstantForDurationOf(precisionThreshold);
@@ -185,9 +162,9 @@ public void formatTo(final StringBuilder buffer, final Instant instant) {
}
};
})
- .collect(Collectors.toList());
+ .toArray(InstantPatternFormatter[]::new);
- switch (formatters.size()) {
+ switch (formatters.length) {
// If found an empty pattern, return an empty formatter
case 0:
@@ -200,17 +177,33 @@ public void formatTo(final StringBuilder buffer, final Instant instant) {
// If extracted a single formatter, return it as is
case 1:
- return formatters.get(0);
+ return formatters[0];
+
+ // Profiling shows that unrolling the generic loop boosts performance
+ case 2:
+ final InstantPatternFormatter first = formatters[0];
+ final InstantPatternFormatter second = formatters[1];
+ return new AbstractFormatter(
+ pattern, locale, timeZone, min(first.getPrecision(), second.getPrecision())) {
+ @Override
+ public void formatTo(StringBuilder buffer, Instant instant) {
+ first.formatTo(buffer, instant);
+ second.formatTo(buffer, instant);
+ }
+ };
// Combine all extracted formatters into one
default:
- final ChronoUnit precision = new CompositePatternSequence(sequences).precision;
+ final ChronoUnit precision = Stream.of(formatters)
+ .map(InstantFormatter::getPrecision)
+ .min(Comparator.comparing(ChronoUnit::getDuration))
+ .get();
return new AbstractFormatter(pattern, locale, timeZone, precision) {
@Override
public void formatTo(final StringBuilder buffer, final Instant instant) {
// noinspection ForLoopReplaceableByForEach (avoid iterator allocation)
- for (int formatterIndex = 0; formatterIndex < formatters.size(); formatterIndex++) {
- final InstantPatternFormatter formatter = formatters.get(formatterIndex);
+ for (int formatterIndex = 0; formatterIndex < formatters.length; formatterIndex++) {
+ final InstantPatternFormatter formatter = formatters[formatterIndex];
formatter.formatTo(buffer, instant);
}
}
@@ -218,10 +211,13 @@ public void formatTo(final StringBuilder buffer, final Instant instant) {
}
}
+ private static ChronoUnit min(ChronoUnit left, ChronoUnit right) {
+ return left.getDuration().compareTo(right.getDuration()) < 0 ? left : right;
+ }
+
static List sequencePattern(final String pattern, final ChronoUnit precisionThreshold) {
List sequences = sequencePattern(pattern);
- final List mergedSequences = mergeDynamicSequences(sequences, precisionThreshold);
- return mergeConsequentEffectivelyConstantSequences(mergedSequences, precisionThreshold);
+ return mergeFactories(sequences, precisionThreshold);
}
private static List sequencePattern(final String pattern) {
@@ -240,7 +236,17 @@ private static List sequencePattern(final String pattern) {
endIndex++;
}
final String sequenceContent = pattern.substring(startIndex, endIndex);
- final PatternSequence sequence = new DynamicPatternSequence(sequenceContent);
+ final PatternSequence sequence;
+ switch (c) {
+ case 's':
+ sequence = new SecondPatternSequence(true, "", 0);
+ break;
+ case 'S':
+ sequence = new SecondPatternSequence(false, "", sequenceContent.length());
+ break;
+ default:
+ sequence = new DynamicPatternSequence(sequenceContent);
+ }
sequences.add(sequence);
startIndex = endIndex;
}
@@ -248,15 +254,7 @@ private static List sequencePattern(final String pattern) {
// Handle single-quotes
else if (c == '\'') {
final int endIndex = pattern.indexOf('\'', startIndex + 1);
- if (endIndex < 0) {
- final String message = String.format(
- "pattern ends with an incomplete string literal that started at index %d: `%s`",
- startIndex, pattern);
- throw new IllegalArgumentException(message);
- }
- final String sequenceLiteral =
- (startIndex + 1) == endIndex ? "'" : pattern.substring(startIndex + 1, endIndex);
- final PatternSequence sequence = new StaticPatternSequence(sequenceLiteral);
+ final PatternSequence sequence = getStaticPatternSequence(pattern, startIndex, endIndex);
sequences.add(sequence);
startIndex = endIndex + 1;
}
@@ -268,243 +266,53 @@ else if (c == '\'') {
startIndex++;
}
}
- return mergeConsequentStaticPatternSequences(sequences);
- }
-
- private static boolean isDynamicPatternLetter(final char c) {
- return "GuyDMLdgQqYwWEecFaBhKkHmsSAnNVvzOXxZ".indexOf(c) >= 0;
+ return sequences;
}
- /**
- * Merges consequent static sequences.
- *
- *
- * For example, the sequencing of the {@code [MM-dd] HH:mm} pattern will create two static sequences for {@code ]} (right brace) and {@code } (whitespace) characters.
- * This method will combine such consequent static sequences into one.
- *
- *
- * Example
- *
- *
- * The {@code [MM-dd] HH:mm} pattern will result in following sequences:
- *
- *
- * {@code
- * [
- * static(literal="["),
- * dynamic(pattern="MM", precision=MONTHS),
- * static(literal="-"),
- * dynamic(pattern="dd", precision=DAYS),
- * static(literal="]"),
- * static(literal=" "),
- * dynamic(pattern="HH", precision=HOURS),
- * static(literal=":"),
- * dynamic(pattern="mm", precision=MINUTES)
- * ]
- * }
- *
- *
- * The above sequencing implies creation of 9 {@link AbstractFormatter}s.
- * This method transforms it to the following:
- *
- *
- * {@code
- * [
- * static(literal="["),
- * dynamic(pattern="MM", precision=MONTHS),
- * static(literal="-"),
- * dynamic(pattern="dd", precision=DAYS),
- * static(literal="] "),
- * dynamic(pattern="HH", precision=HOURS),
- * static(literal=":"),
- * dynamic(pattern="mm", precision=MINUTES)
- * ]
- * }
- *
- *
- * The above sequencing implies creation of 8 {@link AbstractFormatter}s.
- *
- *
- * @param sequences sequences to be transformed
- * @return transformed sequencing where consequent static sequences are merged
- */
- private static List mergeConsequentStaticPatternSequences(final List sequences) {
-
- // Short-circuit if there is nothing to merge
- if (sequences.size() < 2) {
- return sequences;
- }
-
- final List mergedSequences = new ArrayList<>();
- final List accumulatedSequences = new ArrayList<>();
- for (final PatternSequence sequence : sequences) {
-
- // Spotted a static sequence? Stage it for merging.
- if (sequence instanceof StaticPatternSequence) {
- accumulatedSequences.add((StaticPatternSequence) sequence);
- }
-
- // Spotted a dynamic sequence.
- // Merge the accumulated static sequences, and then append the dynamic sequence.
- else {
- mergeConsequentStaticPatternSequences(mergedSequences, accumulatedSequences);
- mergedSequences.add(sequence);
- }
+ private static PatternSequence getStaticPatternSequence(String pattern, int startIndex, int endIndex) {
+ if (endIndex < 0) {
+ final String message = String.format(
+ "pattern ends with an incomplete string literal that started at index %d: `%s`",
+ startIndex, pattern);
+ throw new IllegalArgumentException(message);
}
-
- // Merge leftover static sequences
- mergeConsequentStaticPatternSequences(mergedSequences, accumulatedSequences);
- return mergedSequences;
+ final String sequenceLiteral = (startIndex + 1) == endIndex ? "'" : pattern.substring(startIndex + 1, endIndex);
+ return new StaticPatternSequence(sequenceLiteral);
}
- private static void mergeConsequentStaticPatternSequences(
- final List mergedSequences, final List accumulatedSequences) {
- mergeAccumulatedSequences(mergedSequences, accumulatedSequences, () -> {
- final String literal = accumulatedSequences.stream()
- .map(sequence -> sequence.literal)
- .collect(Collectors.joining());
- return new StaticPatternSequence(literal);
- });
+ private static boolean isDynamicPatternLetter(final char c) {
+ return "GuyDMLdgQqYwWEecFaBhKkHmsSAnNVvzOXxZ".indexOf(c) >= 0;
}
/**
- * Merges the sequences in between the first and the last found dynamic (i.e., non-constant) sequences.
- *
- *
- * For example, given the {@code ss.SSS} pattern – where {@code ss} and {@code SSS} is effectively not constant, yet {@code .} is – this method will combine it into a single dynamic sequence.
- * Because, as demonstrated in {@code DateTimeFormatterSequencingBenchmark}, formatting {@code ss.SSS} is approximately 20% faster than formatting first {@code ss}, then manually appending a {@code .}, and then formatting {@code SSS}.
- *
+ * Merges pattern sequences using {@link PatternSequence#tryMerge}.
*
* Example
*
*
- * Assume {@link #mergeConsequentStaticPatternSequences(List)} produced the following:
+ * For example, given the {@code yyyy-MM-dd'T'HH:mm:ss.SSS} pattern, a precision threshold of {@link ChronoUnit#MINUTES}
+ * and the three implementations ({@link DynamicPatternSequence}, {@link StaticPatternSequence} and
+ * {@link SecondPatternSequence}) from this class,
+ * this method will combine pattern sequences associated with {@code yyyy-MM-dd'T'HH:mm:} into a single sequence,
+ * since these are consecutive and effectively constant sequences.
*
*
* {@code
* [
- * dynamic(pattern="yyyy", precision=YEARS),
+ * dateTimeFormatter(pattern="yyyy", precision=YEARS),
* static(literal="-"),
- * dynamic(pattern="MM", precision=MONTHS),
+ * dateTimeFormatter(pattern="MM", precision=MONTHS),
* static(literal="-"),
- * dynamic(pattern="dd", precision=DAYS),
+ * dateTimeFormatter(pattern="dd", precision=DAYS),
* static(literal="T"),
- * dynamic(pattern="HH", precision=HOURS),
+ * dateTimeFormatter(pattern="HH", precision=HOURS),
* static(literal=":"),
- * dynamic(pattern="mm", precision=MINUTES),
+ * dateTimeFormatter(pattern="mm", precision=MINUTES),
* static(literal=":"),
- * dynamic(pattern="ss", precision=SECONDS),
+ * second(pattern="ss", precision=SECONDS),
* static(literal="."),
- * dynamic(pattern="SSS", precision=MILLISECONDS),
- * dynamic(pattern="X", precision=HOURS),
- * ]
- * }
- *
- *
- * For a threshold precision of {@link ChronoUnit#MINUTES}, this sequencing effectively translates to two {@link DateTimeFormatter#formatTo(TemporalAccessor, Appendable)} invocations for each {@link #formatTo(StringBuilder, Instant)} call: one for {@code ss}, and another one for {@code SSS}.
- * This method transforms the above sequencing into the following:
- *
- *
- * {@code
- * [
- * dynamic(pattern="yyyy", precision=YEARS),
- * static(literal="-"),
- * dynamic(pattern="MM", precision=MONTHS),
- * static(literal="-"),
- * dynamic(pattern="dd", precision=DAYS),
- * static(literal="T"),
- * dynamic(pattern="HH", precision=HOURS),
- * static(literal=":"),
- * dynamic(pattern="mm", precision=MINUTES),
- * static(literal=":"),
- * composite(
- * sequences=[
- * dynamic(pattern="ss", precision=SECONDS),
- * static(literal="."),
- * dynamic(pattern="SSS", precision=MILLISECONDS)
- * ],
- * precision=MILLISECONDS),
- * dynamic(pattern="X", precision=HOURS),
- * ]
- * }
- *
- *
- * The resultant sequencing effectively translates to a single {@link DateTimeFormatter#formatTo(TemporalAccessor, Appendable)} invocation for each {@link #formatTo(StringBuilder, Instant)} call: only one fore {@code ss.SSS}.
- *
- *
- * @param sequences sequences, preferable produced by {@link #mergeConsequentStaticPatternSequences(List)}, to be transformed
- * @param precisionThreshold a precision threshold to determine dynamic (i.e., non-constant) sequences
- * @return transformed sequencing where sequences in between the first and the last found dynamic (i.e., non-constant) sequences are merged
- */
- private static List mergeDynamicSequences(
- final List sequences, final ChronoUnit precisionThreshold) {
-
- // Locate the first and the last dynamic (i.e., non-constant) sequence indices
- int firstDynamicSequenceIndex = -1;
- int lastDynamicSequenceIndex = -1;
- for (int sequenceIndex = 0; sequenceIndex < sequences.size(); sequenceIndex++) {
- final PatternSequence sequence = sequences.get(sequenceIndex);
- final boolean constant = sequence.isConstantForDurationOf(precisionThreshold);
- if (!constant) {
- if (firstDynamicSequenceIndex < 0) {
- firstDynamicSequenceIndex = sequenceIndex;
- }
- lastDynamicSequenceIndex = sequenceIndex;
- }
- }
-
- // Short-circuit if there are less than 2 dynamic sequences
- if (firstDynamicSequenceIndex < 0 || firstDynamicSequenceIndex == lastDynamicSequenceIndex) {
- return sequences;
- }
-
- // Merge dynamic sequences
- final List mergedSequences = new ArrayList<>();
- if (firstDynamicSequenceIndex > 0) {
- mergedSequences.addAll(sequences.subList(0, firstDynamicSequenceIndex));
- }
- final PatternSequence mergedDynamicSequence = new CompositePatternSequence(
- sequences.subList(firstDynamicSequenceIndex, lastDynamicSequenceIndex + 1));
- mergedSequences.add(mergedDynamicSequence);
- if ((lastDynamicSequenceIndex + 1) < sequences.size()) {
- mergedSequences.addAll(sequences.subList(lastDynamicSequenceIndex + 1, sequences.size()));
- }
- return mergedSequences;
- }
-
- /**
- * Merges sequences that are consequent and effectively constant for the provided precision threshold.
- *
- *
- * For example, given the {@code yyyy-MM-dd'T'HH:mm:ss.SSS} pattern and a precision threshold of {@link ChronoUnit#MINUTES}, this method will combine sequences associated with {@code yyyy-MM-dd'T'HH:mm:} into a single sequence, since these are consequent and effectively constant sequences.
- *
- *
- * Example
- *
- *
- * Assume {@link #mergeDynamicSequences(List, ChronoUnit)} produced the following:
- *
- *
- * {@code
- * [
- * dynamic(pattern="yyyy", precision=YEARS),
- * static(literal="-"),
- * dynamic(pattern="MM", precision=MONTHS),
- * static(literal="-"),
- * dynamic(pattern="dd", precision=DAYS),
- * static(literal="T"),
- * dynamic(pattern="HH", precision=HOURS),
- * static(literal=":"),
- * dynamic(pattern="mm", precision=MINUTES),
- * static(literal=":"),
- * composite(
- * sequences=[
- * dynamic(pattern="ss", precision=SECONDS),
- * static(literal="."),
- * dynamic(pattern="SSS", precision=MILLISECONDS)
- * ],
- * precision=MILLISECONDS),
- * dynamic(pattern="X", precision=HOURS),
+ * second(pattern="SSS", precision=MILLISECONDS)
+ * dateTimeFormatter(pattern="X", precision=HOURS),
* ]
* }
*
@@ -515,28 +323,9 @@ private static List mergeDynamicSequences(
*
* {@code
* [
- * composite(
- * sequences=[
- * dynamic(pattern="yyyy", precision=YEARS),
- * static(literal="-"),
- * dynamic(pattern="MM", precision=MONTHS),
- * static(literal="-"),
- * dynamic(pattern="dd", precision=DAYS),
- * static(literal="T"),
- * dynamic(pattern="HH", precision=HOURS),
- * static(literal=":"),
- * dynamic(pattern="mm", precision=MINUTES),
- * static(literal=":")
- * ],
- * precision=MINUTES),
- * composite(
- * sequences=[
- * dynamic(pattern="ss", precision=SECONDS),
- * static(literal="."),
- * dynamic(pattern="SSS", precision=MILLISECONDS)
- * ],
- * precision=MILLISECONDS),
- * dynamic(pattern="X", precision=HOURS),
+ * dateTimeFormatter(pattern="yyyy-MM-dd'T'HH:mm", precision=MINUTES),
+ * second(pattern="ss.SSS", precision=MILLISECONDS),
+ * dateTimeFormatter(pattern="X", precision=MINUTES)
* ]
* }
*
@@ -544,54 +333,32 @@ private static List mergeDynamicSequences(
* The resultant sequencing effectively translates to 3 {@link AbstractFormatter}s.
*
*
- * @param sequences sequences, preferable produced by {@link #mergeDynamicSequences(List, ChronoUnit)}, to be transformed
+ * @param sequences a list of pattern formatter factories
* @param precisionThreshold a precision threshold to determine effectively constant sequences
- * @return transformed sequencing where sequences that are consequent and effectively constant for the provided precision threshold are merged
+ * @return transformed sequencing, where sequences that are effectively constant or effectively dynamic are merged.
*/
- private static List mergeConsequentEffectivelyConstantSequences(
+ private static List mergeFactories(
final List sequences, final ChronoUnit precisionThreshold) {
-
- // Short-circuit if there is nothing to merge
if (sequences.size() < 2) {
return sequences;
}
-
final List mergedSequences = new ArrayList<>();
- boolean accumulatorConstant = true;
- final List accumulatedSequences = new ArrayList<>();
- for (final PatternSequence sequence : sequences) {
- final boolean sequenceConstant = sequence.isConstantForDurationOf(precisionThreshold);
- if (sequenceConstant != accumulatorConstant) {
- mergeConsequentEffectivelyConstantSequences(mergedSequences, accumulatedSequences);
- accumulatorConstant = sequenceConstant;
+ PatternSequence currentFactory = sequences.get(0);
+ for (int i = 1; i < sequences.size(); i++) {
+ PatternSequence nextFactory = sequences.get(i);
+ PatternSequence mergedFactory = currentFactory.tryMerge(nextFactory, precisionThreshold);
+ // The current factory cannot be merged with the next one.
+ if (mergedFactory == null) {
+ mergedSequences.add(currentFactory);
+ currentFactory = nextFactory;
+ } else {
+ currentFactory = mergedFactory;
}
- accumulatedSequences.add(sequence);
}
-
- // Merge the accumulator leftover
- mergeConsequentEffectivelyConstantSequences(mergedSequences, accumulatedSequences);
+ mergedSequences.add(currentFactory);
return mergedSequences;
}
- private static void mergeConsequentEffectivelyConstantSequences(
- final List mergedSequences, final List accumulatedSequences) {
- mergeAccumulatedSequences(
- mergedSequences, accumulatedSequences, () -> new CompositePatternSequence(accumulatedSequences));
- }
-
- private static void mergeAccumulatedSequences(
- final List mergedSequences,
- final List accumulatedSequences,
- final Supplier mergedSequenceSupplier) {
- if (accumulatedSequences.isEmpty()) {
- return;
- }
- final PatternSequence mergedSequence =
- accumulatedSequences.size() == 1 ? accumulatedSequences.get(0) : mergedSequenceSupplier.get();
- mergedSequences.add(mergedSequence);
- accumulatedSequences.clear();
- }
-
private static long toEpochMinutes(final Instant instant) {
return instant.getEpochSecond() / 60;
}
@@ -612,7 +379,7 @@ private abstract static class AbstractFormatter implements InstantPatternFormatt
private final ChronoUnit precision;
- private AbstractFormatter(
+ AbstractFormatter(
final String pattern, final Locale locale, final TimeZone timeZone, final ChronoUnit precision) {
this.pattern = pattern;
this.locale = locale;
@@ -654,22 +421,74 @@ abstract static class PatternSequence {
this.precision = precision;
}
- InstantPatternFormatter createFormatter(final Locale locale, final TimeZone timeZone) {
- final DateTimeFormatter dateTimeFormatter =
- DateTimeFormatter.ofPattern(pattern, locale).withZone(timeZone.toZoneId());
- return new AbstractFormatter(pattern, locale, timeZone, precision) {
- @Override
- public void formatTo(final StringBuilder buffer, final Instant instant) {
- final TemporalAccessor instantAccessor = toTemporalAccessor(instant);
- dateTimeFormatter.formatTo(instantAccessor, buffer);
- }
- };
+ abstract InstantPatternFormatter createFormatter(final Locale locale, final TimeZone timeZone);
+
+ /**
+ * Tries to merge two pattern sequences.
+ *
+ *
+ * If not {@link null}, the pattern sequence returned by this method must:
+ *
+ *
+ * - Have a {@link #precision}, which is the minimum of the precisions of the two merged sequences.
+ * -
+ * Create formatters that are equivalent to the concatenation of the formatters produced by the
+ * two merged sequences.
+ *
+ *
+ *
+ * The returned pattern sequence should try to achieve these two goals:
+ *
+ *
+ * -
+ * Create formatters which are faster than the concatenation of the formatters produced by the
+ * two merged sequences.
+ *
+ * -
+ * It should be {@link null} if one of the pattern sequences is effectively constant over
+ * {@code thresholdPrecision}, but the other one is not.
+ *
+ *
+ *
+ * @param other A pattern sequence.
+ * @param thresholdPrecision A precision threshold to determine effectively constant sequences.
+ * This prevents merging effectively constant and dynamic pattern sequences.
+ * @return A merged formatter factory or {@code null} if merging is not possible.
+ */
+ @Nullable
+ PatternSequence tryMerge(PatternSequence other, ChronoUnit thresholdPrecision) {
+ return null;
}
- private boolean isConstantForDurationOf(final ChronoUnit thresholdPrecision) {
+ boolean isConstantForDurationOf(final ChronoUnit thresholdPrecision) {
return precision.compareTo(thresholdPrecision) >= 0;
}
+ static String escapeLiteral(String literal) {
+ StringBuilder sb = new StringBuilder(literal.length() + 2);
+ boolean inSingleQuotes = false;
+ for (int i = 0; i < literal.length(); i++) {
+ char c = literal.charAt(i);
+ if (c == '\'') {
+ if (inSingleQuotes) {
+ sb.append("'");
+ }
+ inSingleQuotes = false;
+ sb.append("''");
+ } else {
+ if (!inSingleQuotes) {
+ sb.append("'");
+ }
+ inSingleQuotes = true;
+ sb.append(c);
+ }
+ }
+ if (inSingleQuotes) {
+ sb.append("'");
+ }
+ return sb.toString();
+ }
+
@Override
public boolean equals(final Object object) {
if (this == object) {
@@ -689,7 +508,7 @@ public int hashCode() {
@Override
public String toString() {
- return String.format("<%s>%s", pattern, precision);
+ return getClass().getSimpleName() + "[" + "pattern='" + pattern + '\'' + ", precision=" + precision + ']';
}
}
@@ -698,7 +517,7 @@ static final class StaticPatternSequence extends PatternSequence {
private final String literal;
StaticPatternSequence(final String literal) {
- super(literal.equals("'") ? "''" : ("'" + literal + "'"), ChronoUnit.FOREVER);
+ super(escapeLiteral(literal), ChronoUnit.FOREVER);
this.literal = literal;
}
@@ -711,44 +530,109 @@ public void formatTo(final StringBuilder buffer, final Instant instant) {
}
};
}
+
+ @Override
+ @Nullable
+ PatternSequence tryMerge(PatternSequence other, ChronoUnit thresholdPrecision) {
+ // We always merge consecutive static pattern factories
+ if (other instanceof StaticPatternSequence) {
+ final StaticPatternSequence otherStatic = (StaticPatternSequence) other;
+ return new StaticPatternSequence(this.literal + otherStatic.literal);
+ }
+ // We also merge a static pattern factory with a DTF factory
+ if (other instanceof DynamicPatternSequence) {
+ final DynamicPatternSequence otherDtf = (DynamicPatternSequence) other;
+ return new DynamicPatternSequence(this.pattern + otherDtf.pattern, otherDtf.precision);
+ }
+ return null;
+ }
}
+ /**
+ * Creates formatters that use {@link DateTimeFormatter}.
+ */
static final class DynamicPatternSequence extends PatternSequence {
- DynamicPatternSequence(final String content) {
- super(content, contentPrecision(content));
+ /**
+ * @param singlePattern A {@link DateTimeFormatter} pattern containing a single letter.
+ */
+ DynamicPatternSequence(final String singlePattern) {
+ this(singlePattern, patternPrecision(singlePattern));
}
/**
- * @param content a single-letter directive content complying (e.g., {@code H}, {@code HH}, or {@code pHH})
- * @return the time precision of the directive
+ * @param pattern Any {@link DateTimeFormatter} pattern.
+ * @param precision The maximum interval of time over which this pattern is constant.
*/
+ DynamicPatternSequence(final String pattern, final ChronoUnit precision) {
+ super(pattern, precision);
+ }
+
+ InstantPatternFormatter createFormatter(final Locale locale, final TimeZone timeZone) {
+ final DateTimeFormatter dateTimeFormatter =
+ DateTimeFormatter.ofPattern(pattern, locale).withZone(timeZone.toZoneId());
+ return new AbstractFormatter(pattern, locale, timeZone, precision) {
+ @Override
+ public void formatTo(final StringBuilder buffer, final Instant instant) {
+ final TemporalAccessor instantAccessor = toTemporalAccessor(instant);
+ dateTimeFormatter.formatTo(instantAccessor, buffer);
+ }
+ };
+ }
+
+ @Override
@Nullable
- private static ChronoUnit contentPrecision(final String content) {
+ PatternSequence tryMerge(PatternSequence other, ChronoUnit thresholdPrecision) {
+ // We merge two DTF factories if they are both above or below the threshold
+ if (other instanceof DynamicPatternSequence) {
+ final DynamicPatternSequence otherDtf = (DynamicPatternSequence) other;
+ if (isConstantForDurationOf(thresholdPrecision)
+ == otherDtf.isConstantForDurationOf(thresholdPrecision)) {
+ ChronoUnit precision = this.precision.getDuration().compareTo(otherDtf.precision.getDuration()) < 0
+ ? this.precision
+ : otherDtf.precision;
+ return new DynamicPatternSequence(this.pattern + otherDtf.pattern, precision);
+ }
+ }
+ // We merge a static pattern factory
+ if (other instanceof StaticPatternSequence) {
+ final StaticPatternSequence otherStatic = (StaticPatternSequence) other;
+ return new DynamicPatternSequence(this.pattern + otherStatic.pattern, this.precision);
+ }
+ return null;
+ }
- validateContent(content);
- final String paddingRemovedContent = removePadding(content);
+ /**
+ * @param singlePattern a single-letter directive singlePattern complying (e.g., {@code H}, {@code HH}, or {@code pHH})
+ * @return the time precision of the directive
+ */
+ private static ChronoUnit patternPrecision(final String singlePattern) {
- if (paddingRemovedContent.matches("[GuyY]+")) {
+ validateContent(singlePattern);
+ final String paddingRemovedContent = removePadding(singlePattern);
+
+ if (paddingRemovedContent.matches("G+")) {
+ return ChronoUnit.ERAS;
+ } else if (paddingRemovedContent.matches("[uyY]+")) {
return ChronoUnit.YEARS;
} else if (paddingRemovedContent.matches("[MLQq]+")) {
return ChronoUnit.MONTHS;
- } else if (paddingRemovedContent.matches("[wW]+")) {
+ } else if (paddingRemovedContent.matches("w+")) {
return ChronoUnit.WEEKS;
- } else if (paddingRemovedContent.matches("[DdgEecF]+")) {
+ } else if (paddingRemovedContent.matches("[DdgEecFW]+")) {
return ChronoUnit.DAYS;
- } else if (paddingRemovedContent.matches("[aBhKkH]+")
- // Time-zone directives
- || paddingRemovedContent.matches("[ZxXOzvV]+")) {
+ } else if (paddingRemovedContent.matches("[aBhKkH]+")) {
return ChronoUnit.HOURS;
- } else if (paddingRemovedContent.contains("m")) {
+ } else if (paddingRemovedContent.contains("m")
+ // Time-zone directives
+ || paddingRemovedContent.matches("[ZxXOzVv]+")) {
return ChronoUnit.MINUTES;
} else if (paddingRemovedContent.contains("s")) {
return ChronoUnit.SECONDS;
}
// 2 to 3 consequent `S` characters output millisecond precision
- else if (paddingRemovedContent.matches("S{2,3}")
+ else if (paddingRemovedContent.matches("S{1,3}")
// `A` (milli-of-day) outputs millisecond precision.
|| paddingRemovedContent.contains("A")) {
return ChronoUnit.MILLIS;
@@ -759,17 +643,15 @@ else if (paddingRemovedContent.matches("S{4,6}")) {
return ChronoUnit.MICROS;
}
- // A single `S` (fraction-of-second) outputs nanosecond precision
- else if (paddingRemovedContent.equals("S")
- // 7 to 9 consequent `S` characters output nanosecond precision
- || paddingRemovedContent.matches("S{7,9}")
+ // 7 to 9 consequent `S` characters output nanosecond precision
+ else if (paddingRemovedContent.matches("S{7,9}")
// `n` (nano-of-second) and `N` (nano-of-day) always output nanosecond precision.
// This is independent of how many times they occur sequentially.
|| paddingRemovedContent.matches("[nN]+")) {
return ChronoUnit.NANOS;
}
- final String message = String.format("unrecognized pattern: `%s`", content);
+ final String message = String.format("unrecognized pattern: `%s`", singlePattern);
throw new IllegalArgumentException(message);
}
@@ -806,26 +688,125 @@ private static String removePadding(final String content) {
}
}
- static final class CompositePatternSequence extends PatternSequence {
+ static class SecondPatternSequence extends PatternSequence {
+
+ private static final int[] POWERS_OF_TEN = {
+ 100_000_000, 10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1
+ };
+
+ private final boolean printSeconds;
+ private final String separator;
+ private final int fractionalDigits;
+
+ SecondPatternSequence(boolean printSeconds, String separator, int fractionalDigits) {
+ super(
+ createPattern(printSeconds, separator, fractionalDigits),
+ determinePrecision(printSeconds, fractionalDigits));
+ this.printSeconds = printSeconds;
+ this.separator = separator;
+ this.fractionalDigits = fractionalDigits;
+ }
+
+ private static String createPattern(boolean printSeconds, String separator, int fractionalDigits) {
+ StringBuilder builder = new StringBuilder();
+ if (printSeconds) {
+ builder.append("ss");
+ }
+ builder.append(StaticPatternSequence.escapeLiteral(separator));
+ if (fractionalDigits > 0) {
+ builder.append(Strings.repeat("S", fractionalDigits));
+ }
+ return builder.toString();
+ }
+
+ private static ChronoUnit determinePrecision(boolean printSeconds, int digits) {
+ if (digits > 6) return ChronoUnit.NANOS;
+ if (digits > 3) return ChronoUnit.MICROS;
+ if (digits > 0) return ChronoUnit.MILLIS;
+ return printSeconds ? ChronoUnit.SECONDS : ChronoUnit.FOREVER;
+ }
+
+ private static void formatSeconds(StringBuilder buffer, Instant instant) {
+ long secondsInMinute = instant.getEpochSecond() % 60L;
+ buffer.append((char) ((secondsInMinute / 10L) + '0'));
+ buffer.append((char) ((secondsInMinute % 10L) + '0'));
+ }
- CompositePatternSequence(final List sequences) {
- super(concatSequencePatterns(sequences), findSequenceMaxPrecision(sequences));
- // Only allow two or more sequences
- if (sequences.size() < 2) {
- throw new IllegalArgumentException("was expecting two or more sequences: " + sequences);
+ private void formatFractionalDigits(StringBuilder buffer, Instant instant) {
+ int nanos = instant.getNanoOfSecond();
+ // digits contain the first idx digits.
+ int digits;
+ // moreDigits contains the first (idx + 1) digits
+ int moreDigits = 0;
+ // Print the digits
+ for (int idx = 0; idx < fractionalDigits; idx++) {
+ digits = moreDigits;
+ moreDigits = nanos / POWERS_OF_TEN[idx];
+ buffer.append((char) ('0' + moreDigits - 10 * digits));
}
}
- @SuppressWarnings("OptionalGetWithoutIsPresent")
- private static ChronoUnit findSequenceMaxPrecision(List sequences) {
- return sequences.stream()
- .map(sequence -> sequence.precision)
- .min(Comparator.comparing(ChronoUnit::getDuration))
- .get();
+ private static void formatMillis(StringBuilder buffer, Instant instant) {
+ int ms = instant.getNanoOfSecond() / 1_000_000;
+ int cs = ms / 10;
+ int ds = cs / 10;
+ buffer.append((char) ('0' + ds));
+ buffer.append((char) ('0' + cs - 10 * ds));
+ buffer.append((char) ('0' + ms - 10 * cs));
}
- private static String concatSequencePatterns(List sequences) {
- return sequences.stream().map(sequence -> sequence.pattern).collect(Collectors.joining());
+ @Override
+ InstantPatternFormatter createFormatter(Locale locale, TimeZone timeZone) {
+ final BiConsumer fractionDigitsFormatter =
+ fractionalDigits == 3 ? SecondPatternSequence::formatMillis : this::formatFractionalDigits;
+ if (!printSeconds) {
+ return new AbstractFormatter(pattern, locale, timeZone, precision) {
+ @Override
+ public void formatTo(StringBuilder buffer, Instant instant) {
+ buffer.append(separator);
+ fractionDigitsFormatter.accept(buffer, instant);
+ }
+ };
+ }
+ if (fractionalDigits == 0) {
+ return new AbstractFormatter(pattern, locale, timeZone, precision) {
+ @Override
+ public void formatTo(StringBuilder buffer, Instant instant) {
+ formatSeconds(buffer, instant);
+ buffer.append(separator);
+ }
+ };
+ }
+ return new AbstractFormatter(pattern, locale, timeZone, precision) {
+ @Override
+ public void formatTo(StringBuilder buffer, Instant instant) {
+ formatSeconds(buffer, instant);
+ buffer.append(separator);
+ fractionDigitsFormatter.accept(buffer, instant);
+ }
+ };
+ }
+
+ @Override
+ @Nullable
+ PatternSequence tryMerge(PatternSequence other, ChronoUnit thresholdPrecision) {
+ // If we don't have a fractional part, we can merge a literal separator
+ if (other instanceof StaticPatternSequence) {
+ StaticPatternSequence staticOther = (StaticPatternSequence) other;
+ if (fractionalDigits == 0) {
+ return new SecondPatternSequence(
+ printSeconds, this.separator + staticOther.literal, fractionalDigits);
+ }
+ }
+ // We can always append more fractional digits
+ if (other instanceof SecondPatternSequence) {
+ SecondPatternSequence secondOther = (SecondPatternSequence) other;
+ if (!secondOther.printSeconds && secondOther.separator.isEmpty()) {
+ return new SecondPatternSequence(
+ printSeconds, this.separator, this.fractionalDigits + secondOther.fractionalDigits);
+ }
+ }
+ return null;
}
}
}
diff --git a/log4j-perf-test/pom.xml b/log4j-perf-test/pom.xml
index 8b47178c9d4..f16edfad097 100644
--- a/log4j-perf-test/pom.xml
+++ b/log4j-perf-test/pom.xml
@@ -196,6 +196,21 @@
org.apache.maven.plugins
maven-shade-plugin
+
+
+ generate-uber-jar
+
+
+
+
+ org.openjdk.jmh.Main
+ true
+
+
+
+
+
+
diff --git a/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc
index 7b576daabd4..8fc7b16de4a 100644
--- a/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc
@@ -1291,6 +1291,13 @@ unit = "unit" -> (
rounded = "rounded" -> boolean
----
+[NOTE]
+====
+The resolvers based on the `epochConfig` expression are garbage-free.
+
+The resolvers based on the `patternConfig` expression are low-garbage and generate temporary objects only once a minute.
+====
+
.See examples
[%collapsible]
====
diff --git a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
index 0e135285cad..d3f713e7942 100644
--- a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
@@ -1607,7 +1607,9 @@ Format modifiers to control such things as field width, padding, left, and right
|<>
-|Only the predefined date formats (`DEFAULT`, `ISO8601`, `UNIX`, `UNIX_MILLIS`, etc.) are garbage-free
+|
+The numeric formats (`UNIX` and `UNIX_MILLIS`) are garbage-free.
+The remaining formats are low-garbage and only generate temporary objects once per minute.
|<>