Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -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':
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -461,7 +464,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"));
Expand All @@ -470,7 +473,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
Expand Down Expand Up @@ -584,6 +587,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"));
Expand Down
Loading