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"));

Reply via email to