拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 字符串格式中的命名占位符

字符串格式中的命名占位符

白鹭 - 2022-08-22 2411 0 2

一、概述

Java 标准库提供了String.format()方法来格式化基于模板的字符串,例如:String.format(“%s is awesome”, “Java”)

在本教程中,我们将探讨如何使字符串格式支持命名参数。

2. 问题介绍

String.format()方法使用起来非常简单。但是,当format()调用有很多参数时,很难理解哪个值将来自哪个格式说明符,例如:

Employee e = ...; // get an employee instance
String template = "Firstname: %s, Lastname: %s, Id: %s, Company: %s, Role: %s, Department: %s, Address: %s ...";
String.format(template, e.firstName, e.lastName, e.Id, e.company, e.department, e.role ... )

此外,当我们将这些参数传递给方法时,它很容易出错。例如,在上面的示例中,我们错误地将e.department放在了e.role之前。

因此,如果我们可以在模板中使用命名参数之类的东西,然后通过包含所有参数name->value映射的Map应用格式,那就太好了:

String template = "Firstname: ${firstname}, Lastname: ${lastname}, Id: ${id} ...";
ourFormatMethod.format(template, parameterMap);

在本教程中,我们将首先看一个使用流行的外部库的解决方案,它可以解决这个问题的大多数情况。然后,我们将讨论一个破坏解决方案的边缘情况。

最后,我们将创建自己的format()方法来涵盖所有情况。

为简单起见,我们将使用单元测试断言来验证方法是否返回预期的字符串。

还值得一提的是,在本教程中我们将只关注简单的字符串格式( %s)不支持其他格式类型,例如日期、数字或具有定义宽度和精度的格式。

3. 使用 Apache Commons TextStrSubstitutor

Apache Commons Text 库包含许多用于处理字符串的便捷实用程序。它附带StrSubstitutor,它允许我们根据命名参数进行字符串替换。

首先,让我们将该库作为新的依赖项添加到我们的Maven 配置文件中:

<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>

当然,我们总能在Maven 中央存储库中找到最新版本

在我们了解如何使用StrSubstitutor类之前,让我们创建一个模板作为示例:

String TEMPLATE = "Text: [${text}] Number: [${number}] Text again: [${text}]";

接下来,让我们创建一个测试,使用StrSubstitutor基于上面的模板构建一个字符串:

Map<String, Object> params = new HashMap<>();
params.put("text", "It's awesome!");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "${", "}");
assertThat(result).isEqualTo("Text: [It's awesome!] Number: [42] Text again: [It's awesome!]");

如测试代码所示,我们让params保存所有name -> value映射。当我们调用StrSubstitutor.replace()方法时,除了templateparams,我们还传递前缀和后缀来告知StrSubstitutor参数在模板中包含什么StrSubstitutor将搜索参数名称的prefix + map.entry.key + suffix

当我们运行测试时,它通过了。所以,StrSubstitutor似乎解决了这个问题。

4. 边缘案例:当替换包含占位符时

我们已经看到StrSubstitutor.replace()测试通过了我们的基本用例。但是,某些特殊情况不在测试范围内。例如,参数值可能包含参数名称模式“ ${ … }”。

现在,让我们测试一下这个案例:

Map<String, Object> params = new HashMap<>();
params.put("text", "'${number}' is a placeholder.");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "${", "}");

assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");

在上面的测试中,参数“ ${text}”的值包含文本“ ${number}”。所以,我们期望“ ${text}”被文本“ ${number}”替换。

但是,如果我们执行它,测试就会失败:

org.opentest4j.AssertionFailedError:
expected: "Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]"
but was: "Text: ['42' is a placeholder.] Number: [42] Text again: ['42' is a placeholder.]"

因此,StrSubstitutor将文字${number}视为参数占位符。

事实上,StrSubstitutor的Javadoc 已经说明了这种情况:

变量替换以递归方式工作。因此,如果一个变量值包含一个变量,那么该变量也将被替换。

发生这种情况是因为,在每个递归步骤中,StrSubstitutor将最后一个替换结果作为新template继续进行进一步的替换

为了绕过这个问题,我们可以选择不同的前缀和后缀,这样它们就不会受到干扰:

String TEMPLATE = "Text: [%{text}] Number: [%{number}] Text again: [%{text}]";
Map<String, Object> params = new HashMap<>();
params.put("text", "'${number}' is a placeholder.");
params.put("number", 42);
String result = StrSubstitutor.replace(TEMPLATE, params, "%{", "}");

assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");

但是,从理论上讲,由于我们无法预测值,因此值总是可能包含参数名称模式并干扰替换。

接下来,让我们创建自己的format()方法来解决问题。

5. 自行构建格式化程序

我们已经讨论了为什么StrSubstitutor不能很好地处理边缘情况。因此,如果我们创建一个方法,困难在于我们不应该使用循环或递归来将最后一步的结果作为当前步骤的新输入

5.1。解决问题的思路

这个想法是我们在模板中搜索参数名称模式。但是,当我们找到一个时,我们不会立即将其替换为地图中的值。相反,我们构建了一个可用于标准String.format()方法的新模板。如果我们举个例子,我们将尝试转换:

String TEMPLATE = "Text: [${text}] Number: [${number}] Text again: [${text}]";
Map<String, Object> params ...

进入:

String NEW_TEMPLATE = "Text: [%s] Number: [%s] Text again: [%s]";
List<Object> valueList = List.of("'${number}' is a placeholder.", 42, "'${number}' is a placeholder.");

然后,我们可以调用String.format(NEW_TEMPLATE, valueList.toArray());完成工作。

5.2.创建方法

接下来,让我们创建一个方法来实现这个想法:

public static String format(String template, Map<String, Object> parameters) {
StringBuilder newTemplate = new StringBuilder(template);
List<Object> valueList = new ArrayList<>();

Matcher matcher = Pattern.compile("[$][{](\\w+)}").matcher(template);

while (matcher.find()) {
String key = matcher.group(1);

String paramName = "${" + key + "}";
int index = newTemplate.indexOf(paramName);
if (index != -1) {
newTemplate.replace(index, index + paramName.length(), "%s");
valueList.add(parameters.get(key));
}
}

return String.format(newTemplate.toString(), valueList.toArray());
}

上面的代码非常简单。让我们快速浏览一下以了解它是如何工作的。

首先,我们声明了两个新变量来保存新模板( newTemplate) 和值列表( valueList)。稍后我们调用String.format()时将需要它们。

我们使用Regex在模板中定位参数名称模式。然后,我们将参数名称模式替换为“%s”,并将相应的值添加到valueList变量中。

最后,我们使用新转换的模板和valueList.的值调用String.format()

为简单起见,我们在方法中硬编码了前缀“ ${”和后缀“ }”。此外,如果未提供参数“ ${unknown}”的值,我们只需将“ ${unknown}”参数替换为“ null

5.3.测试我们的format()方法

接下来,让我们测试该方法是否适用于常规情况:

Map<String, Object> params = new HashMap<>();
params.put("text", "It's awesome!");
params.put("number", 42);
String result = NamedFormatter.format(TEMPLATE, params);
assertThat(result).isEqualTo("Text: [It's awesome!] Number: [42] Text again: [It's awesome!]");

同样,如果我们试一试,测试就会通过。

当然,我们想看看它是否也适用于边缘情况:

params.put("text", "'${number}' is a placeholder.");
result = NamedFormatter.format(TEMPLATE, params);
assertThat(result).isEqualTo("Text: ['${number}' is a placeholder.] Number: [42] Text again: ['${number}' is a placeholder.]");

如果我们执行这个测试,它也通过了!我们已经解决了这个问题。

六,结论

在本文中,我们探讨了如何从一组值中替换基于模板的字符串中的参数。基本上,Apache Commons Text 的StrSubstitutor.replace()方法使用起来非常简单,可以解决大多数情况。但是,当值包含参数名称模式时,StrSubstitutor可能会产生意外结果。

因此,我们实现了一个format()方法来解决这种极端情况。



标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *