This is an automated email from the ASF dual-hosted git repository.

mattsicker pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/main by this push:
     new ebfc571f7d Use garbage-free formatter for `s` and `S` patterns (#3338) 
(#3860)
ebfc571f7d is described below

commit ebfc571f7db6bc78eda12df5aaa9eb4e9178174d
Author: Matt Sicker <mattsic...@apache.org>
AuthorDate: Thu Jul 31 13:08:22 2025 -0500

    Use garbage-free formatter for `s` and `S` patterns (#3338) (#3860)
    
    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: Piotr P. Karwasz <piotr.git...@karwasz.org>
    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..d6fb4ccd2a 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}]

Reply via email to