From 007b9831ae6c25974f5fb3ff2d7aa78d2aeffbb3 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" <piotr.github@karwasz.org> Date: Sun, 16 Feb 2025 10:25:55 +0100 Subject: [PATCH 1/5] Fix formatting of `s` pattern The specialized `SecondPatternSequence` formatter is optimized to format the `ss.?S*` patterns, where seconds are padded. For the not padded (and uncommon) `s` pattern, we should use the usual `DynamicPatternSequence`. --- .../instant/InstantPatternDynamicFormatterTest.java | 7 ++++++- .../internal/instant/InstantPatternDynamicFormatter.java | 6 +++++- 2 files changed, 11 insertions(+), 2 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 7829d234aaa..b8faab32ddf 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 @@ -100,6 +100,9 @@ static List<Arguments> sequencingTestCases() { testCases.add(Arguments.of( "HH:mm:ss.SSSSSS", ChronoUnit.MINUTES, asList(pDyn("HH':'mm':'", ChronoUnit.MINUTES), pSec(".", 6)))); + // Seconds without padding + testCases.add(Arguments.of("s.SSS", ChronoUnit.SECONDS, asList(pDyn("s'.'", ChronoUnit.SECONDS), pMilliSec()))); + return testCases; } @@ -352,7 +355,9 @@ static Stream<Arguments> formatterInputs() { "yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'HH:mm:ss.SSSSSS", "dd/MM/yy HH:mm:ss.SSS", - "dd/MM/yyyy HH:mm:ss.SSS") + "dd/MM/yyyy HH:mm:ss.SSS", + // seconds without padding + "s.SSS") .flatMap(InstantPatternDynamicFormatterTest::formatterInputs); } 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 bb8059329ea..c0e14ecdbeb 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 @@ -239,7 +239,11 @@ private static List<PatternSequence> sequencePattern(final String pattern) { final PatternSequence sequence; switch (c) { case 's': - sequence = new SecondPatternSequence(true, "", 0); + if (sequenceContent.length() == 2) { + sequence = new SecondPatternSequence(true, "", 0); + } else { + sequence = new DynamicPatternSequence(sequenceContent); + } break; case 'S': sequence = new SecondPatternSequence(false, "", sequenceContent.length()); From 34597ddf1c87294da6b1e7e6a544fc355de66d9d Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" <piotr.github@karwasz.org> Date: Tue, 18 Feb 2025 22:34:13 +0100 Subject: [PATCH 2/5] Add support for unpadded seconds --- .../InstantPatternDynamicFormatterTest.java | 24 ++++--- .../InstantPatternDynamicFormatter.java | 69 +++++++++++-------- 2 files changed, 52 insertions(+), 41 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 b8faab32ddf..307511f5bd4 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 @@ -65,13 +65,13 @@ static List<Arguments> sequencingTestCases() { testCases.add(Arguments.of( "yyyyMMddHHmmssSSSX", ChronoUnit.HOURS, - asList(pDyn("yyyyMMddHH", ChronoUnit.HOURS), pDyn("mm"), pSec("", 3), pDyn("X")))); + asList(pDyn("yyyyMMddHH", ChronoUnit.HOURS), pDyn("mm"), pSec(2, "", 3), pDyn("X")))); // `yyyyMMddHHmmssSSSX` instant cache updated per minute testCases.add(Arguments.of( "yyyyMMddHHmmssSSSX", ChronoUnit.MINUTES, - asList(pDyn("yyyyMMddHHmm", ChronoUnit.MINUTES), pSec("", 3), pDyn("X")))); + asList(pDyn("yyyyMMddHHmm", ChronoUnit.MINUTES), pSec(2, "", 3), pDyn("X")))); // ISO9601 instant cache updated daily final String iso8601InstantPattern = "yyyy-MM-dd'T'HH:mm:ss.SSSX"; @@ -81,27 +81,29 @@ static List<Arguments> sequencingTestCases() { asList( pDyn("yyyy'-'MM'-'dd'T'", ChronoUnit.DAYS), pDyn("HH':'mm':'", ChronoUnit.MINUTES), - pSec(".", 3), + pSec(2, ".", 3), pDyn("X")))); // ISO9601 instant cache updated per minute testCases.add(Arguments.of( iso8601InstantPattern, ChronoUnit.MINUTES, - asList(pDyn("yyyy'-'MM'-'dd'T'HH':'mm':'", ChronoUnit.MINUTES), pSec(".", 3), pDyn("X")))); + asList(pDyn("yyyy'-'MM'-'dd'T'HH':'mm':'", ChronoUnit.MINUTES), pSec(2, ".", 3), pDyn("X")))); // ISO9601 instant cache updated per second testCases.add(Arguments.of( iso8601InstantPattern, ChronoUnit.SECONDS, - asList(pDyn("yyyy'-'MM'-'dd'T'HH':'mm':'", ChronoUnit.MINUTES), pSec(".", 3), pDyn("X")))); + asList(pDyn("yyyy'-'MM'-'dd'T'HH':'mm':'", ChronoUnit.MINUTES), pSec(2, ".", 3), pDyn("X")))); // Seconds and micros testCases.add(Arguments.of( - "HH:mm:ss.SSSSSS", ChronoUnit.MINUTES, asList(pDyn("HH':'mm':'", ChronoUnit.MINUTES), pSec(".", 6)))); + "HH:mm:ss.SSSSSS", + ChronoUnit.MINUTES, + asList(pDyn("HH':'mm':'", ChronoUnit.MINUTES), pSec(2, ".", 6)))); // Seconds without padding - testCases.add(Arguments.of("s.SSS", ChronoUnit.SECONDS, asList(pDyn("s'.'", ChronoUnit.SECONDS), pMilliSec()))); + testCases.add(Arguments.of("s.SSS", ChronoUnit.SECONDS, singletonList(pSec(1, ".", 3)))); return testCases; } @@ -114,12 +116,12 @@ private static DynamicPatternSequence pDyn(final String pattern, final ChronoUni return new DynamicPatternSequence(pattern, precision); } - private static SecondPatternSequence pSec(String separator, int fractionalDigits) { - return new SecondPatternSequence(true, separator, fractionalDigits); + private static SecondPatternSequence pSec(int secondDigits, String separator, int fractionalDigits) { + return new SecondPatternSequence(secondDigits, separator, fractionalDigits); } private static SecondPatternSequence pMilliSec() { - return new SecondPatternSequence(false, "", 3); + return new SecondPatternSequence(0, "", 3); } @ParameterizedTest @@ -369,7 +371,7 @@ static Stream<Arguments> formatterInputs() { Arrays.stream(TimeZone.getAvailableIDs()).map(TimeZone::getTimeZone).toArray(TimeZone[]::new); static Stream<Arguments> formatterInputs(final String pattern) { - return IntStream.range(0, 500).mapToObj(ignoredIndex -> { + return IntStream.range(0, 100).mapToObj(ignoredIndex -> { final Locale locale = LOCALES[RANDOM.nextInt(LOCALES.length)]; final TimeZone timeZone = TIME_ZONES[RANDOM.nextInt(TIME_ZONES.length)]; final MutableInstant instant = randomInstant(); 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 c0e14ecdbeb..1d1732ef230 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 @@ -239,14 +239,10 @@ private static List<PatternSequence> sequencePattern(final String pattern) { final PatternSequence sequence; switch (c) { case 's': - if (sequenceContent.length() == 2) { - sequence = new SecondPatternSequence(true, "", 0); - } else { - sequence = new DynamicPatternSequence(sequenceContent); - } + sequence = new SecondPatternSequence(sequenceContent.length(), "", 0); break; case 'S': - sequence = new SecondPatternSequence(false, "", sequenceContent.length()); + sequence = new SecondPatternSequence(0, "", sequenceContent.length()); break; default: sequence = new DynamicPatternSequence(sequenceContent); @@ -698,39 +694,50 @@ static class SecondPatternSequence extends PatternSequence { 100_000_000, 10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1 }; - private final boolean printSeconds; + private final int secondDigits; private final String separator; private final int fractionalDigits; - SecondPatternSequence(boolean printSeconds, String separator, int fractionalDigits) { + SecondPatternSequence(int secondDigits, String separator, int fractionalDigits) { super( - createPattern(printSeconds, separator, fractionalDigits), - determinePrecision(printSeconds, fractionalDigits)); - this.printSeconds = printSeconds; + createPattern(secondDigits, separator, fractionalDigits), + determinePrecision(secondDigits, fractionalDigits)); + if (secondDigits < 0 || secondDigits > 2) { + throw new IllegalArgumentException("Unsupported number of `s` pattern letters."); + } + if (fractionalDigits > 9) { + throw new IllegalArgumentException("Unsupported number of `S` pattern letters."); + } + this.secondDigits = secondDigits; 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 String createPattern(int secondDigits, String separator, int fractionalDigits) { + return Strings.repeat("s", secondDigits) + + StaticPatternSequence.escapeLiteral(separator) + + Strings.repeat("S", fractionalDigits); } - private static ChronoUnit determinePrecision(boolean printSeconds, int digits) { + private static ChronoUnit determinePrecision(int secondDigits, 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; + return secondDigits > 0 ? ChronoUnit.SECONDS : ChronoUnit.FOREVER; + } + + private void formatSeconds(StringBuilder buffer, Instant instant) { + switch (secondDigits) { + case 1: + buffer.append(instant.getEpochSecond() % 60L); + break; + case 2: + formatPaddedSeconds(buffer, instant); + break; + } } - private static void formatSeconds(StringBuilder buffer, Instant instant) { + private static void formatPaddedSeconds(StringBuilder buffer, Instant instant) { long secondsInMinute = instant.getEpochSecond() % 60L; buffer.append((char) ((secondsInMinute / 10L) + '0')); buffer.append((char) ((secondsInMinute % 10L) + '0')); @@ -761,9 +768,11 @@ private static void formatMillis(StringBuilder buffer, Instant instant) { @Override InstantPatternFormatter createFormatter(Locale locale, TimeZone timeZone) { + final BiConsumer<StringBuilder, Instant> secondDigitsFormatter = + secondDigits == 2 ? SecondPatternSequence::formatPaddedSeconds : this::formatSeconds; final BiConsumer<StringBuilder, Instant> fractionDigitsFormatter = fractionalDigits == 3 ? SecondPatternSequence::formatMillis : this::formatFractionalDigits; - if (!printSeconds) { + if (secondDigits == 0) { return new AbstractFormatter(pattern, locale, timeZone, precision) { @Override public void formatTo(StringBuilder buffer, Instant instant) { @@ -776,7 +785,7 @@ public void formatTo(StringBuilder buffer, Instant instant) { return new AbstractFormatter(pattern, locale, timeZone, precision) { @Override public void formatTo(StringBuilder buffer, Instant instant) { - formatSeconds(buffer, instant); + secondDigitsFormatter.accept(buffer, instant); buffer.append(separator); } }; @@ -784,7 +793,7 @@ public void formatTo(StringBuilder buffer, Instant instant) { return new AbstractFormatter(pattern, locale, timeZone, precision) { @Override public void formatTo(StringBuilder buffer, Instant instant) { - formatSeconds(buffer, instant); + secondDigitsFormatter.accept(buffer, instant); buffer.append(separator); fractionDigitsFormatter.accept(buffer, instant); } @@ -799,15 +808,15 @@ PatternSequence tryMerge(PatternSequence other, ChronoUnit thresholdPrecision) { StaticPatternSequence staticOther = (StaticPatternSequence) other; if (fractionalDigits == 0) { return new SecondPatternSequence( - printSeconds, this.separator + staticOther.literal, fractionalDigits); + this.secondDigits, 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()) { + if (secondOther.secondDigits == 0 && secondOther.separator.isEmpty()) { return new SecondPatternSequence( - printSeconds, this.separator, this.fractionalDigits + secondOther.fractionalDigits); + this.secondDigits, this.separator, this.fractionalDigits + secondOther.fractionalDigits); } } return null; From 04c56a9a1f9592be4c25a9c386d6f807525f0438 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" <piotr.github@karwasz.org> Date: Wed, 19 Feb 2025 11:09:12 +0100 Subject: [PATCH 3/5] Improve unpadded formatting --- .../instant/InstantPatternDynamicFormatter.java | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) 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 1d1732ef230..8b0b411f144 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 @@ -726,15 +726,8 @@ private static ChronoUnit determinePrecision(int secondDigits, int digits) { return secondDigits > 0 ? ChronoUnit.SECONDS : ChronoUnit.FOREVER; } - private void formatSeconds(StringBuilder buffer, Instant instant) { - switch (secondDigits) { - case 1: - buffer.append(instant.getEpochSecond() % 60L); - break; - case 2: - formatPaddedSeconds(buffer, instant); - break; - } + private static void formatUnpaddedSeconds(StringBuilder buffer, Instant instant) { + buffer.append(instant.getEpochSecond() % 60L); } private static void formatPaddedSeconds(StringBuilder buffer, Instant instant) { @@ -768,8 +761,9 @@ private static void formatMillis(StringBuilder buffer, Instant instant) { @Override InstantPatternFormatter createFormatter(Locale locale, TimeZone timeZone) { - final BiConsumer<StringBuilder, Instant> secondDigitsFormatter = - secondDigits == 2 ? SecondPatternSequence::formatPaddedSeconds : this::formatSeconds; + final BiConsumer<StringBuilder, Instant> secondDigitsFormatter = secondDigits == 2 + ? SecondPatternSequence::formatPaddedSeconds + : SecondPatternSequence::formatUnpaddedSeconds; final BiConsumer<StringBuilder, Instant> fractionDigitsFormatter = fractionalDigits == 3 ? SecondPatternSequence::formatMillis : this::formatFractionalDigits; if (secondDigits == 0) { From 0870bacc0ce4298f3d7901b5f98e447e0b0fa8bd Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" <piotr.github@karwasz.org> Date: Wed, 19 Feb 2025 14:17:19 +0100 Subject: [PATCH 4/5] Apply review suggestions (1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Volkan Yazıcı <volkan@yazi.ci> --- .../instant/InstantPatternDynamicFormatter.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) 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 8b0b411f144..85cca7a6f91 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 @@ -702,11 +702,19 @@ static class SecondPatternSequence extends PatternSequence { super( createPattern(secondDigits, separator, fractionalDigits), determinePrecision(secondDigits, fractionalDigits)); - if (secondDigits < 0 || secondDigits > 2) { - throw new IllegalArgumentException("Unsupported number of `s` pattern letters."); + final int maxSecondDigits = 2; + if (secondDigits > maxSecondDigits) { + final String message = String.format( + "More than %d `s` pattern letters are not supported, found: %d", + maxSecondDigits, secondDigits); + throw new IllegalArgumentException(message); } - if (fractionalDigits > 9) { - throw new IllegalArgumentException("Unsupported number of `S` pattern letters."); + final int maxFractionalDigits = 9; + if (fractionalDigits > maxFractionalDigits) { + final String message = String.format( + "More than %d `S` pattern letters are not supported, found: %d", + maxFractionalDigits, fractionalDigits); + throw new IllegalArgumentException(message); } this.secondDigits = secondDigits; this.separator = separator; From 5ee2f120c3c4a8f7b21b511d750a7df7291d9e34 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" <piotr.github@karwasz.org> Date: Wed, 19 Feb 2025 14:29:01 +0100 Subject: [PATCH 5/5] Formatting --- .../util/internal/instant/InstantPatternDynamicFormatter.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 85cca7a6f91..1c9dfab5711 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 @@ -705,8 +705,7 @@ static class SecondPatternSequence extends PatternSequence { final int maxSecondDigits = 2; if (secondDigits > maxSecondDigits) { final String message = String.format( - "More than %d `s` pattern letters are not supported, found: %d", - maxSecondDigits, secondDigits); + "More than %d `s` pattern letters are not supported, found: %d", maxSecondDigits, secondDigits); throw new IllegalArgumentException(message); } final int maxFractionalDigits = 9;