This is an automated email from the ASF dual-hosted git repository.
mattsicker pushed a commit to branch feature/3.x/port-3338
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git
commit 3a9166525ea9b00a3cfa043955e9139a97ac3ae1
Author: Piotr P. Karwasz <piotr.git...@karwasz.org>
AuthorDate: Tue Dec 31 10:53:59 2024 +0100
Use garbage-free formatter for `s` and `S` patterns (#3338)
This PR improves #3139, by introducing a new `InstantPatternFormatter` for
patterns of the form "ss\.S{n}". Unlike the previous formatter based on
`DateTimeFormatter`, the formatter is garbage-free.
We also simplify the merging algorithm for pattern formatter factories, by
moving the merging logic to the pattern formatter factories themselves.
This PR does not contain a separate change log entry, since #3139 has not
been published yet.
Fixes #3337.
Co-authored-by: Volkan Yazıcı <vol...@yazi.ci>
---
.../InstantPatternDynamicFormatterTest.java | 175 ++---
...stantPatternThreadLocalCachedFormatterTest.java | 2 +-
.../instant/InstantPatternDynamicFormatter.java | 713 ++++++++++-----------
.../ROOT/pages/manual/json-template-layout.adoc | 7 +
.../modules/ROOT/pages/manual/pattern-layout.adoc | 4 +-
5 files changed, 459 insertions(+), 442 deletions(-)
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 ddb8c31103..d3f80a3607 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.TimeZone;
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 @@ public class InstantPatternDynamicFormatterTest {
static List<Arguments> sequencingTestCases() {
final List<Arguments> 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 @@ public class InstantPatternDynamicFormatterTest {
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 @@ public class InstantPatternDynamicFormatterTest {
"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 @@ public class InstantPatternDynamicFormatterTest {
assertPatternPrecision(pattern, ChronoUnit.SECONDS);
}
- @ParameterizedTest
- @ValueSource(
- strings = {
+ static Stream<String> should_recognize_patterns_of_minute_precision() {
+ Stream<String> 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 @@ public class InstantPatternDynamicFormatterTest {
"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<Arguments>
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 @@ public class InstantPatternDynamicFormatterTest {
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 b25fb85d74..1f94b0b9ae 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 @@ class InstantPatternThreadLocalCachedFormatterTest {
}
@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 a5486f7a50..225ee8dce0 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.Locale;
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 @@ import org.jspecify.annotations.Nullable;
* <li>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</li>
* <li>Upon a formatting request, combine the cached outputs with the dynamic
parts (i.e., {@code ss.SSS})</li>
* </ol>
- * <h2>Implementation note</h2>
- * <p>
- * Formatting can actually even be made faster and garbage-free by manually
formatting sub-minute precision directives as follows:
- * </p>
- * <pre>{@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();
- * }</pre>
- * <p>
- * 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.
- * </p>
*
* @since 2.25.0
*/
@@ -165,7 +142,7 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
// Sequence the pattern and create associated formatters
final List<PatternSequence> sequences = sequencePattern(pattern,
precisionThreshold);
- final List<InstantPatternFormatter> 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 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
}
};
})
- .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 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
// 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 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
}
}
+ private static ChronoUnit min(ChronoUnit left, ChronoUnit right) {
+ return left.getDuration().compareTo(right.getDuration()) < 0 ? left :
right;
+ }
+
static List<PatternSequence> sequencePattern(final String pattern, final
ChronoUnit precisionThreshold) {
List<PatternSequence> sequences = sequencePattern(pattern);
- final List<PatternSequence> mergedSequences =
mergeDynamicSequences(sequences, precisionThreshold);
- return mergeConsequentEffectivelyConstantSequences(mergedSequences,
precisionThreshold);
+ return mergeFactories(sequences, precisionThreshold);
}
private static List<PatternSequence> sequencePattern(final String pattern)
{
@@ -240,7 +236,17 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
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 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
// 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 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
startIndex++;
}
}
- return mergeConsequentStaticPatternSequences(sequences);
- }
-
- private static boolean isDynamicPatternLetter(final char c) {
- return "GuyDMLdgQqYwWEecFaBhKkHmsSAnNVvzOXxZ".indexOf(c) >= 0;
+ return sequences;
}
- /**
- * Merges consequent static sequences.
- *
- * <p>
- * 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.
- * </p>
- *
- * <h2>Example</h2>
- *
- * <p>
- * The {@code [MM-dd] HH:mm} pattern will result in following sequences:
- * </p>
- *
- * <pre>{@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)
- * ]
- * }</pre>
- *
- * <p>
- * The above sequencing implies creation of 9 {@link AbstractFormatter}s.
- * This method transforms it to the following:
- * </p>
- *
- * <pre>{@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)
- * ]
- * }</pre>
- *
- * <p>
- * The above sequencing implies creation of 8 {@link AbstractFormatter}s.
- * </p>
- *
- * @param sequences sequences to be transformed
- * @return transformed sequencing where consequent static sequences are
merged
- */
- private static List<PatternSequence>
mergeConsequentStaticPatternSequences(final List<PatternSequence> sequences) {
-
- // Short-circuit if there is nothing to merge
- if (sequences.size() < 2) {
- return sequences;
- }
-
- final List<PatternSequence> mergedSequences = new ArrayList<>();
- final List<StaticPatternSequence> 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<PatternSequence> mergedSequences, final
List<StaticPatternSequence> 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.
- *
- * <p>
- * 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}.
- * </p>
+ * Merges pattern sequences using {@link PatternSequence#tryMerge}.
*
* <h2>Example</h2>
*
* <p>
- * 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.
* </p>
*
* <pre>{@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),
- * ]
- * }</pre>
- *
- * <p>
- * 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:
- * </p>
- *
- * <pre>{@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),
- * ]
- * }</pre>
- *
- * <p>
- * 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}.
- * </p>
- *
- * @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<PatternSequence> mergeDynamicSequences(
- final List<PatternSequence> 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<PatternSequence> 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.
- *
- * <p>
- * 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.
- * </p>
- *
- * <h2>Example</h2>
- *
- * <p>
- * Assume {@link #mergeDynamicSequences(List, ChronoUnit)} produced the
following:
- * </p>
- *
- * <pre>{@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),
* ]
* }</pre>
*
@@ -515,28 +323,9 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
*
* <pre>{@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)
* ]
* }</pre>
*
@@ -544,54 +333,32 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
* The resultant sequencing effectively translates to 3 {@link
AbstractFormatter}s.
* </p>
*
- * @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<PatternSequence>
mergeConsequentEffectivelyConstantSequences(
+ private static List<PatternSequence> mergeFactories(
final List<PatternSequence> sequences, final ChronoUnit
precisionThreshold) {
-
- // Short-circuit if there is nothing to merge
if (sequences.size() < 2) {
return sequences;
}
-
final List<PatternSequence> mergedSequences = new ArrayList<>();
- boolean accumulatorConstant = true;
- final List<PatternSequence> 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<PatternSequence> mergedSequences, final
List<PatternSequence> accumulatedSequences) {
- mergeAccumulatedSequences(
- mergedSequences, accumulatedSequences, () -> new
CompositePatternSequence(accumulatedSequences));
- }
-
- private static <S extends PatternSequence> void mergeAccumulatedSequences(
- final List<PatternSequence> mergedSequences,
- final List<S> accumulatedSequences,
- final Supplier<PatternSequence> 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 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
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 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
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.
+ *
+ * <p>
+ * If not {@link null}, the pattern sequence returned by this
method must:
+ * </p>
+ * <ol>
+ * <li>Have a {@link #precision}, which is the minimum of the
precisions of the two merged sequences.</li>
+ * <li>
+ * Create formatters that are equivalent to the concatenation
of the formatters produced by the
+ * two merged sequences.
+ * </li>
+ * </ol>
+ * <p>
+ * The returned pattern sequence should try to achieve these two
goals:
+ * </p>
+ * <ol>
+ * <li>
+ * Create formatters which are faster than the concatenation
of the formatters produced by the
+ * two merged sequences.
+ * </li>
+ * <li>
+ * It should be {@link null} if one of the pattern sequences
is effectively constant over
+ * {@code thresholdPrecision}, but the other one is not.
+ * </li>
+ * </ol>
+ *
+ * @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 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
@Override
public String toString() {
- return String.format("<%s>%s", pattern, precision);
+ return getClass().getSimpleName() + "[" + "pattern='" + pattern +
'\'' + ", precision=" + precision + ']';
}
}
@@ -698,7 +517,7 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
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 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
}
};
}
+
+ @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 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
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 @@ final class InstantPatternDynamicFormatter implements
InstantPatternFormatter {
}
}
- 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<PatternSequence> 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<PatternSequence> 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<PatternSequence>
sequences) {
- return sequences.stream().map(sequence ->
sequence.pattern).collect(Collectors.joining());
+ @Override
+ InstantPatternFormatter createFormatter(Locale locale, TimeZone
timeZone) {
+ final BiConsumer<StringBuilder, Instant> 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/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc
b/src/site/antora/modules/ROOT/pages/manual/json-template-layout.adoc
index 6f20199718..ff79bfad9c 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
@@ -1263,6 +1263,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 a1e01e809d..ee9115a2ed 100644
--- a/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/pattern-layout.adoc
@@ -1571,7 +1571,9 @@ Format modifiers to control such things as field width,
padding, left, and right
|xref:#converter-date[%d +
%date]
-|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.
|xref:#converter-encode[%enc\{pattern} +
%encode\{pattern}]