Improve StringWithPlaceholders

This commit is contained in:
filoghost 2021-05-24 18:36:21 +02:00
parent 3f14a67b73
commit 9e05bb61cb
2 changed files with 71 additions and 48 deletions

View File

@ -17,15 +17,15 @@ public class StringWithPlaceholders {
private static final char PLACEHOLDER_START_CHAR = '{';
private final String string;
private final List<PlaceholderMatch> placeholderMatches;
private final List<StringPart> stringParts;
public StringWithPlaceholders(String string) {
this.string = string;
this.placeholderMatches = findPlaceholders(string);
this.stringParts = toStringParts(string);
}
public boolean containsPlaceholders() {
return placeholderMatches != null && !placeholderMatches.isEmpty();
return stringParts != null;
}
public String replacePlaceholders(Function<PlaceholderOccurrence, String> replaceFunction) {
@ -34,61 +34,45 @@ public class StringWithPlaceholders {
}
StringBuilder output = new StringBuilder();
int lastAppendIndex = 0;
for (StringPart part : stringParts) {
output.append(part.getValue(replaceFunction));
}
for (PlaceholderMatch match : placeholderMatches) {
// Append leading text (if any)
if (lastAppendIndex != match.startIndex) {
output.append(string, lastAppendIndex, match.startIndex);
}
String replacement = replaceFunction.apply(match.content);
if (replacement != null) {
// Append placeholder replacement
output.append(replacement);
} else {
// If no replacement is provided, do not replace the occurrence
output.append(match.unparsedString);
}
lastAppendIndex = match.endIndex;
}
// Append trailing text (if any)
if (lastAppendIndex < string.length()) {
output.append(string, lastAppendIndex, string.length());
}
return output.toString();
}
private @Nullable List<PlaceholderMatch> findPlaceholders(String input) {
int currentIndex = 0;
private @Nullable List<StringPart> toStringParts(String string) {
int placeholderStartIndex = -1;
List<PlaceholderMatch> matches = null;
int lastAppendIndex = 0;
List<StringPart> stringParts = null; // Lazy initialization
while (currentIndex < input.length()) {
char currentChar = input.charAt(currentIndex);
for (int currentIndex = 0; currentIndex < string.length(); currentIndex++) {
char currentChar = string.charAt(currentIndex);
if (placeholderStartIndex >= 0) {
// Inside placeholder
if (currentChar == PLACEHOLDER_END_CHAR) {
int endIndex = currentIndex + 1;
// The unparsed string includes the opening and closing tags (e.g.: "{online: lobby}")
String unparsedString = input.substring(placeholderStartIndex, endIndex);
String unparsedString = string.substring(placeholderStartIndex, endIndex);
// The content string does NOT include the opening and closing tags (e.g.: "online: lobby")
String contentString = unparsedString.substring(1, unparsedString.length() - 1);
PlaceholderOccurrence content = PlaceholderOccurrence.parse(contentString);
if (matches == null) {
matches = new ArrayList<>();
if (stringParts == null) {
stringParts = new ArrayList<>();
}
// Append leading literal part (if any)
if (placeholderStartIndex != lastAppendIndex) {
stringParts.add(new LiteralStringPart(string.substring(lastAppendIndex, placeholderStartIndex)));
}
matches.add(new PlaceholderMatch(
content,
unparsedString,
placeholderStartIndex,
endIndex));
// Append placeholder part
stringParts.add(new PlaceholderStringPart(content, unparsedString));
lastAppendIndex = endIndex;
placeholderStartIndex = -1;
} else if (currentChar == PLACEHOLDER_START_CHAR) {
@ -96,30 +80,64 @@ public class StringWithPlaceholders {
placeholderStartIndex = currentIndex;
}
} else {
// Outside placeholders, just look for the start of a placeholder
if (currentChar == PLACEHOLDER_START_CHAR) {
placeholderStartIndex = currentIndex;
}
}
currentIndex++;
}
return matches;
// Append trailing literal part (if any)
if (lastAppendIndex != string.length() && stringParts != null) {
stringParts.add(new LiteralStringPart(string.substring(lastAppendIndex)));
}
return stringParts;
}
private interface StringPart {
String getValue(Function<PlaceholderOccurrence, String> placeholderReplaceFunction);
}
private static class LiteralStringPart implements StringPart {
private final String literalString;
public LiteralStringPart(String literalString) {
this.literalString = literalString;
}
@Override
public String getValue(Function<PlaceholderOccurrence, String> placeholderReplaceFunction) {
return literalString;
}
}
private static class PlaceholderMatch {
private static class PlaceholderStringPart implements StringPart {
private final PlaceholderOccurrence content;
private final String unparsedString;
private final int startIndex;
private final int endIndex;
PlaceholderMatch(PlaceholderOccurrence parsedContent, String unparsedString, int startIndex, int endIndex) {
PlaceholderStringPart(PlaceholderOccurrence parsedContent, String unparsedString) {
this.content = parsedContent;
this.unparsedString = unparsedString;
this.startIndex = startIndex;
this.endIndex = endIndex;
}
@Override
public String getValue(Function<PlaceholderOccurrence, String> placeholderReplaceFunction) {
String replacement = placeholderReplaceFunction.apply(content);
if (replacement != null) {
return replacement;
} else {
// If no replacement is provided, leave the unparsed placeholder string
return unparsedString;
}
}
}

View File

@ -21,14 +21,19 @@ class StringWithPlaceholdersTest {
@ParameterizedTest(name = "[{index}] {0} -> {1}")
@MethodSource("replacementsTestArguments")
void replacements(String input, String expectedOutput) {
boolean expectedContainsPlaceholders = expectedOutput.contains("#");
StringWithPlaceholders s = new StringWithPlaceholders(input);
assertThat(s.replacePlaceholders(occurrence -> "#")).isEqualTo(expectedOutput);
assertThat(s.containsPlaceholders()).isEqualTo(expectedContainsPlaceholders);
}
static Stream<Arguments> replacementsTestArguments() {
return Stream.of(
Arguments.of("", ""),
Arguments.of("{}", "#"), // Empty placeholder should still be detected
Arguments.of("{p}{p}", "##"),
Arguments.of(" {p} ", " # "),
Arguments.of("{p} {p} {p}", "# # #"),
Arguments.of("{{p}}", "{#}"), // Only the innermost placeholder should be replaced
Arguments.of("{p abc", "{p abc"), // Placeholder without closing tag