This is an automated email from the ASF dual-hosted git repository.
garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-lang.git
The following commit(s) were added to refs/heads/master by this push:
new 68ee18aa5 [LANG-1827] Support escaped single quotes ('') in
DurationFormatUtils format patterns (#1700)
68ee18aa5 is described below
commit 68ee18aa5ab14e1ec8ddd4a80fd7f43860fd874c
Author: Sarankumar Baskar <[email protected]>
AuthorDate: Wed Jul 1 01:55:59 2026 +0530
[LANG-1827] Support escaped single quotes ('') in DurationFormatUtils
format patterns (#1700)
* Support escaped single quotes ('') in DurationFormatUtils format patterns
* [LANG-1827] Parameterize escaped quote tests
Run each DurationFormatUtils escaped quote case independently.
---------
Co-authored-by: Sarankumar Baskar <[email protected]>
---
.../commons/lang3/time/DurationFormatUtils.java | 29 +++++++++++++-----
.../lang3/time/DurationFormatUtilsTest.java | 35 ++++++++++++++++++++--
2 files changed, 55 insertions(+), 9 deletions(-)
diff --git
a/src/main/java/org/apache/commons/lang3/time/DurationFormatUtils.java
b/src/main/java/org/apache/commons/lang3/time/DurationFormatUtils.java
index 07a53b922..8f04f2b95 100644
--- a/src/main/java/org/apache/commons/lang3/time/DurationFormatUtils.java
+++ b/src/main/java/org/apache/commons/lang3/time/DurationFormatUtils.java
@@ -44,9 +44,10 @@
* <tr><td>s</td><td>seconds</td></tr>
* <tr><td>S</td><td>milliseconds</td></tr>
* <tr><td>'text'</td><td>arbitrary text content</td></tr>
+ * <tr><td>''</td><td>literal single quote (apostrophe)</td></tr>
* </table>
*
- * <strong>Note: It's not currently possible to include a single-quote in a
format.</strong>
+ * A literal single quote is represented by a pair of consecutive single
quotes ({@code ''}).
* <p>
* Token values are printed using decimal digits.
* A token character can be repeated to ensure that the field occupies a
certain minimum
@@ -669,7 +670,6 @@ static Token[] lexx(final String format) {
}
String value = null;
switch (ch) {
- // TODO: Need to handle escaping of '
case '[':
if (inOptional) {
throw new IllegalArgumentException("Nested optional block
at index: " + i);
@@ -685,12 +685,27 @@ static Token[] lexx(final String format) {
break;
case '\'':
if (inLiteral) {
- buffer = null;
- inLiteral = false;
+ if (i + 1 < format.length() && format.charAt(i + 1) ==
'\'') {
+ // escaped quote '' ? append literal apostrophe, stay
in literal
+ buffer.append('\'');
+ i++;
+ } else {
+ // end of literal
+ buffer = null;
+ inLiteral = false;
+ }
} else {
- buffer = new StringBuilder();
- list.add(new Token(buffer, inOptional, optionalIndex));
- inLiteral = true;
+ if (i + 1 < format.length() && format.charAt(i + 1) ==
'\'') {
+ // standalone '' outside a literal ? emit a single
apostrophe
+ buffer = new StringBuilder("'");
+ list.add(new Token(buffer, inOptional, optionalIndex));
+ buffer = null;
+ i++;
+ } else {
+ buffer = new StringBuilder();
+ list.add(new Token(buffer, inOptional, optionalIndex));
+ inLiteral = true;
+ }
}
break;
case 'y':
diff --git
a/src/test/java/org/apache/commons/lang3/time/DurationFormatUtilsTest.java
b/src/test/java/org/apache/commons/lang3/time/DurationFormatUtilsTest.java
index b96bdb741..a7155f4f8 100644
--- a/src/test/java/org/apache/commons/lang3/time/DurationFormatUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/time/DurationFormatUtilsTest.java
@@ -34,6 +34,9 @@
import org.apache.commons.lang3.AbstractLangTest;
import org.apache.commons.lang3.time.DurationFormatUtils.Token;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
import org.junitpioneer.jupiter.DefaultTimeZone;
/**
@@ -471,7 +474,7 @@ void testFormatPeriod() {
cal.set(Calendar.MILLISECOND, 0);
time = cal.getTime().getTime();
assertEquals("40", DurationFormatUtils.formatPeriod(time1970, time,
"yM"));
- assertEquals("4 years 0 months",
DurationFormatUtils.formatPeriod(time1970, time, "y' ''years' M 'months'"));
+ assertEquals("4 'years 0 months",
DurationFormatUtils.formatPeriod(time1970, time, "y' ''years' M 'months'"));
assertEquals("4 years 0 months",
DurationFormatUtils.formatPeriod(time1970, time, "y' years 'M' months'"));
assertEquals("4years 0months",
DurationFormatUtils.formatPeriod(time1970, time, "y'years 'M'months'"));
assertEquals("04/00", DurationFormatUtils.formatPeriod(time1970, time,
"yy/MM"));
@@ -480,7 +483,7 @@ void testFormatPeriod() {
assertEquals("048", DurationFormatUtils.formatPeriod(time1970, time,
"MMM"));
// no date in result
assertEquals("hello", DurationFormatUtils.formatPeriod(time1970, time,
"'hello'"));
- assertEquals("helloworld", DurationFormatUtils.formatPeriod(time1970,
time, "'hello''world'"));
+ assertEquals("hello'world", DurationFormatUtils.formatPeriod(time1970,
time, "'hello''world'"));
}
@Test
@@ -616,6 +619,34 @@ void testLANG815() {
void testLANG981() { // unmatched quote char in lexx
assertIllegalArgumentException(() ->
DurationFormatUtils.lexx("'yMdHms''S"));
}
+
+ private static Arguments[] testLANG1827() {
+ final long twoHours = Duration.ofHours(2).toMillis();
+ final long twoHoursThirtyMin =
Duration.ofHours(2).plusMinutes(30).toMillis();
+ final long oneDayTwoHours = Duration.ofDays(1).plusHours(2).toMillis();
+ return new Arguments[] {
+ Arguments.of("escaped quote inside literal", "2 o'clock",
twoHours, "H' o''clock'"),
+ Arguments.of("escaped quote at start of literal", "it's 2
hours", twoHours, "'it''s 'H' hours'"),
+ Arguments.of("escaped quote outside literal", "2'30",
twoHoursThirtyMin, "H''m"),
+ Arguments.of("multiple escaped quotes", "it's been 1 day's and
2 hour's", oneDayTwoHours,
+ "'it''s been 'd' day''s and 'H' hour''s'"),
+ Arguments.of("standalone escaped quote", "2h'30m",
twoHoursThirtyMin, "H'h'''m'm'"),
+ Arguments.of("escaped quote inside optional block", "2
hour's", twoHours, "[d' day''s ']H' hour''s'"),
+ Arguments.of("existing literal behavior", "2 hours 30
minutes", twoHoursThirtyMin, "H' hours 'm' minutes'")
+ };
+ }
+
+ @ParameterizedTest
+ @MethodSource
+ void testLANG1827(final String label, final String expected, final long
durationMillis, final String format) {
+ assertEquals(expected,
DurationFormatUtils.formatDuration(durationMillis, format), label);
+ }
+
+ @Test
+ void testLANG1827UnmatchedQuote() {
+ assertIllegalArgumentException(() ->
DurationFormatUtils.lexx("'unmatched"));
+ }
+
@Test
void testLANG982() { // More than 3 millisecond digits following a second
assertEquals("61.999", DurationFormatUtils.formatDuration(61999,
"s.S"));