Skip to content

Commit e3c6f6d

Browse files
committed
fix: dynamically compute legacy date format pattern
This change: * Restores detailed documentation explaining the differences between `FixedDateFormat` and `DateTimeFormatter` patterns. * Implements dynamic computation of the legacy pattern based on the documented rules, ensuring consistent behavior and improved maintainability.
1 parent 3799799 commit e3c6f6d

File tree

2 files changed

+76
-22
lines changed

2 files changed

+76
-22
lines changed

log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/DatePatternConverter.java

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,8 @@ private static String readPattern(@Nullable final String[] options) {
109109
* @since 2.25.0
110110
*/
111111
static String decodeNamedPattern(final String pattern) {
112-
// `FixedDateFormat` accepted two types of patterns:
113-
// - the names of `FixedFormat` enum constants (identical to `NamedInstantPattern` enum names),
114-
// - or custom pattern strings.
115-
//
116-
// To determine the format's precision, we cannot return the legacy name directly;
117-
// instead, we must return the equivalent `FixedDateFormat` pattern string.
118-
// These patterns are only recognized by `FixedDateFormat` so we make them available only
119-
// via the package-private `getLegacyPattern()` method.
112+
// See `NamedInstantPattern.getLegacyPattern()`
113+
// for the difference between legacy and `DateTimeFormatter` patterns.
120114
try {
121115
NamedInstantPattern namedInstantPattern = NamedInstantPattern.valueOf(pattern);
122116
return InstantPatternFormatter.LEGACY_FORMATTERS_ENABLED

log4j-core/src/main/java/org/apache/logging/log4j/core/pattern/NamedInstantPattern.java

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
*/
1717
package org.apache.logging.log4j.core.pattern;
1818

19+
import java.util.regex.Pattern;
1920
import org.jspecify.annotations.NullMarked;
2021

2122
/**
@@ -28,9 +29,9 @@
2829
public enum NamedInstantPattern {
2930
ABSOLUTE("HH:mm:ss,SSS"),
3031

31-
ABSOLUTE_MICROS("HH:mm:ss,SSSSSS", "HH:mm:ss,nnnnnn"),
32+
ABSOLUTE_MICROS("HH:mm:ss,SSSSSS"),
3233

33-
ABSOLUTE_NANOS("HH:mm:ss,SSSSSSSSS", "HH:mm:ss,nnnnnnnnn"),
34+
ABSOLUTE_NANOS("HH:mm:ss,SSSSSSSSS"),
3435

3536
ABSOLUTE_PERIOD("HH:mm:ss.SSS"),
3637

@@ -42,9 +43,9 @@ public enum NamedInstantPattern {
4243

4344
DEFAULT("yyyy-MM-dd HH:mm:ss,SSS"),
4445

45-
DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,SSSSSS", "yyyy-MM-dd HH:mm:ss,nnnnnn"),
46+
DEFAULT_MICROS("yyyy-MM-dd HH:mm:ss,SSSSSS"),
4647

47-
DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,SSSSSSSSS", "yyyy-MM-dd HH:mm:ss,nnnnnnnnn"),
48+
DEFAULT_NANOS("yyyy-MM-dd HH:mm:ss,SSSSSSSSS"),
4849

4950
DEFAULT_PERIOD("yyyy-MM-dd HH:mm:ss.SSS"),
5051

@@ -54,30 +55,26 @@ public enum NamedInstantPattern {
5455

5556
ISO8601("yyyy-MM-dd'T'HH:mm:ss,SSS"),
5657

57-
ISO8601_OFFSET_DATE_TIME_HH("yyyy-MM-dd'T'HH:mm:ss,SSSx", "yyyy-MM-dd'T'HH:mm:ss,SSSX"),
58+
ISO8601_OFFSET_DATE_TIME_HH("yyyy-MM-dd'T'HH:mm:ss,SSSx"),
5859

59-
ISO8601_OFFSET_DATE_TIME_HHMM("yyyy-MM-dd'T'HH:mm:ss,SSSxx", "yyyy-MM-dd'T'HH:mm:ss,SSSXX"),
60+
ISO8601_OFFSET_DATE_TIME_HHMM("yyyy-MM-dd'T'HH:mm:ss,SSSxx"),
6061

61-
ISO8601_OFFSET_DATE_TIME_HHCMM("yyyy-MM-dd'T'HH:mm:ss,SSSxxx", "yyyy-MM-dd'T'HH:mm:ss,SSSXXX"),
62+
ISO8601_OFFSET_DATE_TIME_HHCMM("yyyy-MM-dd'T'HH:mm:ss,SSSxxx"),
6263

6364
ISO8601_PERIOD("yyyy-MM-dd'T'HH:mm:ss.SSS"),
6465

65-
ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.SSSSSS", "yyyy-MM-dd'T'HH:mm:ss.nnnnnn"),
66+
ISO8601_PERIOD_MICROS("yyyy-MM-dd'T'HH:mm:ss.SSSSSS"),
6667

6768
US_MONTH_DAY_YEAR2_TIME("dd/MM/yy HH:mm:ss.SSS"),
6869

6970
US_MONTH_DAY_YEAR4_TIME("dd/MM/yyyy HH:mm:ss.SSS");
7071

72+
private static final Pattern NANO_PATTERN = Pattern.compile("S{4,}");
73+
7174
private final String pattern;
72-
private final String legacyPattern;
7375

7476
NamedInstantPattern(String pattern) {
75-
this(pattern, pattern);
76-
}
77-
78-
NamedInstantPattern(String pattern, String legacyPattern) {
7977
this.pattern = pattern;
80-
this.legacyPattern = legacyPattern;
8178
}
8279

8380
/**
@@ -93,11 +90,74 @@ public String getPattern() {
9390
/**
9491
* Returns the legacy {@link org.apache.logging.log4j.core.util.datetime.FixedDateFormat} pattern
9592
* associated with this named pattern.
93+
* <p>
94+
* If legacy formatters are enabled, output is produced for
95+
* {@code FixedDateFormat} and {@code FastDateFormat}. To convert the {@code DateTimeFormatter}
96+
* to its legacy counterpart, the following transformations need to be applied:
97+
* </p>
98+
* <table>
99+
* <caption>Pattern Differences</caption>
100+
* <thead>
101+
* <tr>
102+
* <th></th>
103+
* <th>Microseconds</th>
104+
* <th>Nanoseconds</th>
105+
* <th>Time-zone</th>
106+
* </tr>
107+
* </thead>
108+
* <tbody>
109+
* <tr>
110+
* <td>Legacy formatter directive</td>
111+
* <td><code>nnnnnn</code></td>
112+
* <td><code>nnnnnnnnn</code></td>
113+
* <td><code>X</code>, <code>XX</code>, <code>XXX</code></td>
114+
* </tr>
115+
* <tr>
116+
* <td>{@code DateTimeFormatter} directive</td>
117+
* <td><code>SSSSSS</code></td>
118+
* <td><code>SSSSSSSSS</code></td>
119+
* <td><code>x</code>, <code>xx</code>, <code>xxx</code></td>
120+
* </tr>
121+
* </tbody>
122+
* </table>
123+
* <h2></h2>
124+
* <ul>
125+
* <li>
126+
* <p>
127+
* Legacy formatters are largely compatible with the {@code SimpleDateFormat} specification,
128+
* but introduce a custom {@code n} pattern letter, unique to Log4j, to represent sub-millisecond precision.
129+
* This {@code n} is not part of the standard {@code SimpleDateFormat}.
130+
* </p>
131+
* <p>
132+
* In legacy formatters, repeating {@code n} increases the precision, similar to how repeated {@code S}
133+
* is used for fractional seconds in {@code DateTimeFormatter}.
134+
* </p>
135+
* <p>
136+
* In contrast, {@code DateTimeFormatter} interprets {@code n} as nano-of-second.
137+
* In Java 17, both {@code n} and {@code N} always output nanosecond precision,
138+
* regardless of the number of pattern letters.
139+
* </p>
140+
* </li>
141+
* <li>
142+
* <p>
143+
* Legacy formatters use <code>X</code>, <code>XX</code>, and <code>XXX</code> to format time zones as
144+
* <code>+00</code>, <code>+0000</code>, or <code>+00:00</code>, following {@code SimpleDateFormat} conventions.
145+
* In contrast, {@code DateTimeFormatter} outputs <code>Z</code> for zero-offset when using <code>X</code>.
146+
* To ensure numeric output for zero-offset (e.g., <code>+00</code>),
147+
* we use <code>x</code>, <code>xx</code>, or <code>xxx</code> instead.
148+
* </p>
149+
* </li>
150+
* </ul>
96151
*
97152
* @return the legacy pattern string as used in
98153
* {@link org.apache.logging.log4j.core.util.datetime.FixedDateFormat.FixedFormat}
99154
*/
100155
String getLegacyPattern() {
156+
String legacyPattern = pattern.replace('x', 'X');
157+
if (NANO_PATTERN.matcher(pattern).find()) {
158+
// If the pattern contains sub-millis, replace 'S' with 'n' for legacy formatters
159+
return legacyPattern.replace('S', 'n');
160+
}
101161
return legacyPattern;
102162
}
103163
}

0 commit comments

Comments
 (0)