On this page
自定义数字和日期/时间格式
Page Contents
Overview
Note:
自 FreeMarker 2.3.24 起存在自定义格式(此处描述的那种格式)。
FreeMarker 允许您定义自己的数字和日期/时间/日期时间格式,并为它们关联一个名称。此机制有几个应用程序:
自定义格式化程序算法:您可以使用自己的格式化程序算法,而不必依赖 FreeMarker 提供的算法。为此,实现
freemarker.core.TemplateNumberFormatFactory
或freemarker.core.TemplateDateFormatFactory
。您将找到有关below的一些示例。别名:您可以使用
AliasTemplateNumberFormatFactory
和AliasTemplateDateFormatFactory
将应用程序特定的名称(如“价格”,“重量”,“ fileDate”,“ logEventTime”等)赋予其他格式。因此,模板可以只引用该名称,例如${lastModified?string.@fileDate}
,而不必直接指定格式。因此,可以在单个中心位置(配置 FreeMarker)上指定格式,而不必在模板中重复指定格式。因此,模板作者也不必 Importing 复杂且难以记住的格式设置模式。 见下面的例子。模型敏感的格式设置:应用程序可以将自定义
freemarker.TemplateModel
-s 放入数据模型中,而不是将普通值(例如int
-s,double
-s 等)放入其中,以将与渲染相关的信息附加到该值。自定义格式器可以使用此信息(例如,在数字后显示单位),因为它们接收的是TemplateModel
本身,而不是包装的原始值。 见下面的例子。打印标记而不是纯文本的格式:您可能希望在格式化值中使用 HTML 标记(或其他标记),例如将负数涂成红色或将 HTML
sup
元素用作指数。如果您编写的自定义格式如前所述,但在格式化程序类中覆盖format
方法,以便它返回TemplateMarkupOutputModel
而不是String
,则可以这样做。 (您不应该只将标记返回为String
,否则它可能会被转义;请参阅auto-escaping。)
可以使用custom_number_formats
和custom_date_formats
配置设置注册自定义格式。之后,在可以使用String
指定格式的任何地方,现在都可以将自定义格式称为"@name"
。因此,例如,如果您使用"smart"
注册了数字格式实现,则可以将number_format
设置(Configurable.setNumberFormat(String)
)设置为"@smart"
,或者在模板中发布${n?string.@smart}
或<#setting number_format="@smart">
。此外,您可以为自定义格式定义参数,例如"@smart 2"
,参数的解释取决于格式化程序的实现。
简单的自定义数字格式示例
此自定义数字格式以十六进制形式显示数字:
package com.example;
import java.util.Locale;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
import freemarker.template.utility.NumberUtil;
public class HexTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
public static final HexTemplateNumberFormatFactory INSTANCE
= new HexTemplateNumberFormatFactory();
private HexTemplateNumberFormatFactory() {
// Defined to decrease visibility
}
@Override
public TemplateNumberFormat get(String params, Locale locale, Environment env)
throws InvalidFormatParametersException {
TemplateFormatUtil.checkHasNoParameters(params);
return HexTemplateNumberFormat.INSTANCE;
}
private static class HexTemplateNumberFormat extends TemplateNumberFormat {
private static final HexTemplateNumberFormat INSTANCE = new HexTemplateNumberFormat();
private HexTemplateNumberFormat() { }
@Override
public String formatToPlainText(TemplateNumberModel numberModel)
throws UnformattableValueException, TemplateModelException {
Number n = TemplateFormatUtil.getNonNullNumber(numberModel);
try {
return Integer.toHexString(NumberUtil.toIntExact(n));
} catch (ArithmeticException e) {
throw new UnformattableValueException(n + " doesn't fit into an int");
}
}
@Override
public boolean isLocaleBound() {
return false;
}
@Override
public String getDescription() {
return "hexadecimal int";
}
}
}
我们将以上格式注册为“ hex”:
// Where you initalize the application-wide Configuration singleton:
Configuration cfg = ...;
...
Map<String, TemplateNumberFormatFactory> customNumberFormats = ...;
...
customNumberFormats.put("hex", HexTemplateNumberFormatFactory.INSTANCE);
...
cfg.setCustomNumberFormats(customNumberFormats);
现在我们可以在模板中使用此格式:
${x?string.@hex}
甚至将其设置为默认数字格式:
cfg.setNumberFormat("@hex");
高级自定义数字格式示例
这是一种更复杂的自定义数字格式,显示了如何处理格式字符串中的参数,以及如何委派给另一种格式:
package com.example;
import java.util.Locale;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
import freemarker.template.utility.NumberUtil;
import freemarker.template.utility.StringUtil;
/**
* Shows a number in base N number system. Can only format numbers that fit into an {@code int},
* however, optionally you can specify a fallback format. This format has one required parameter,
* the numerical system base. That can be optionally followed by "|" and a fallback format.
*/
public class BaseNTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
public static final BaseNTemplateNumberFormatFactory INSTANCE
= new BaseNTemplateNumberFormatFactory();
private BaseNTemplateNumberFormatFactory() {
// Defined to decrease visibility
}
@Override
public TemplateNumberFormat get(String params, Locale locale, Environment env)
throws InvalidFormatParametersException {
TemplateNumberFormat fallbackFormat;
{
int barIdx = params.indexOf('|');
if (barIdx != -1) {
String fallbackFormatStr = params.substring(barIdx + 1);
params = params.substring(0, barIdx);
try {
fallbackFormat = env.getTemplateNumberFormat(fallbackFormatStr, locale);
} catch (TemplateValueFormatException e) {
throw new InvalidFormatParametersException(
"Couldn't get the fallback number format (specified after the \"|\"), "
+ StringUtil.jQuote(fallbackFormatStr) + ". Reason: " + e.getMessage(),
e);
}
} else {
fallbackFormat = null;
}
}
int base;
try {
base = Integer.parseInt(params);
} catch (NumberFormatException e) {
if (params.length() == 0) {
throw new InvalidFormatParametersException(
"A format parameter is required to specify the numerical system base.");
}
throw new InvalidFormatParametersException(
"The format paramter must be an integer, but was (shown quoted): "
+ StringUtil.jQuote(params));
}
if (base < 2) {
throw new InvalidFormatParametersException("A base must be at least 2.");
}
return new BaseNTemplateNumberFormat(base, fallbackFormat);
}
private static class BaseNTemplateNumberFormat extends TemplateNumberFormat {
private final int base;
private final TemplateNumberFormat fallbackFormat;
private BaseNTemplateNumberFormat(int base, TemplateNumberFormat fallbackFormat) {
this.base = base;
this.fallbackFormat = fallbackFormat;
}
@Override
public String formatToPlainText(TemplateNumberModel numberModel)
throws TemplateModelException, TemplateValueFormatException {
Number n = TemplateFormatUtil.getNonNullNumber(numberModel);
try {
return Integer.toString(NumberUtil.toIntExact(n), base);
} catch (ArithmeticException e) {
if (fallbackFormat == null) {
throw new UnformattableValueException(
n + " doesn't fit into an int, and there was no fallback format "
+ "specified.");
} else {
return fallbackFormat.formatToPlainText(numberModel);
}
}
}
@Override
public boolean isLocaleBound() {
return false;
}
@Override
public String getDescription() {
return "base " + base;
}
}
}
我们将上述格式注册为名称“ base”:
// Where you initalize the application-wide Configuration singleton:
Configuration cfg = ...;
...
Map<String, TemplateNumberFormatFactory> customNumberFormats = ...;
...
customNumberFormats.put("base", BaseNTemplateNumberFormatFactory.INSTANCE);
...
cfg.setCustomNumberFormats(customNumberFormats);
现在我们可以在模板中使用此格式:
${x?string.@base_8}
在此上方,参数字符串为"8"
,因为 FreeMarker 允许使用_
而不是空格将其与格式名称分开,因此您不必编写更长的n?string["@base 8"]
格式。
当然,我们也可以将其设置为默认数字格式,例如:
cfg.setNumberFormat("@base 8");
以下是使用备用 Numbers 格式(即"0.0###"
)的示例:
cfg.setNumberFormat("@base 8|0.0###");
请注意,具有|
语法以及全部功能的此功能是在前面的示例代码中完全实现的。
自定义日期/时间格式示例
这种简单的日期格式将日期/时间值格式化为自纪元以来的毫秒数:
package com.example;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import freemarker.template.TemplateDateModel;
import freemarker.template.TemplateModelException;
public class EpochMillisTemplateDateFormatFactory extends TemplateDateFormatFactory {
public static final EpochMillisTemplateDateFormatFactory INSTANCE
= new EpochMillisTemplateDateFormatFactory();
private EpochMillisTemplateDateFormatFactory() {
// Defined to decrease visibility
}
@Override
public TemplateDateFormat get(String params, int dateType,
Locale locale, TimeZone timeZone, boolean zonelessInput,
Environment env)
throws InvalidFormatParametersException {
TemplateFormatUtil.checkHasNoParameters(params);
return EpochMillisTemplateDateFormat.INSTANCE;
}
private static class EpochMillisTemplateDateFormat extends TemplateDateFormat {
private static final EpochMillisTemplateDateFormat INSTANCE
= new EpochMillisTemplateDateFormat();
private EpochMillisTemplateDateFormat() { }
@Override
public String formatToPlainText(TemplateDateModel dateModel)
throws UnformattableValueException, TemplateModelException {
return String.valueOf(TemplateFormatUtil.getNonNullDate(dateModel).getTime());
}
@Override
public boolean isLocaleBound() {
return false;
}
@Override
public boolean isTimeZoneBound() {
return false;
}
@Override
public Date parse(String s, int dateType) throws UnparsableValueException {
try {
return new Date(Long.parseLong(s));
} catch (NumberFormatException e) {
throw new UnparsableValueException("Malformed long");
}
}
@Override
public String getDescription() {
return "millis since the epoch";
}
}
}
我们将上述格式注册为“ epoch”:
// Where you initalize the application-wide Configuration singleton:
Configuration cfg = ...;
...
Map<String, TemplateDateFormatFactory> customDateFormats = ...;
...
customDateFormats.put("epoch", EpochMillisTemplateDateFormatFactory.INSTANCE);
...
cfg.setCustomDateFormats(customDateFormats);
现在我们可以在模板中使用此格式:
${t?string.@epoch}
当然,我们也可以将其设置为默认的日期时间格式,例如:
cfg.setDateTimeFormat("@epoch");
有关例如使用格式参数的更复杂信息,请参考高级数字格式示例。使用日期格式执行此操作非常相似。
别名格式示例
在此示例中,我们指定一些数字格式和日期格式,它们是另一种格式的别名:
// Where you initalize the application-wide Configuration singleton:
Configuration cfg = ...;
Map<String, TemplateNumberFormatFactory> customNumberFormats
= new HashMap<String, TemplateNumberFormatFactory>();
customNumberFormats.put("price", new AliasTemplateNumberFormatFactory(",000.00"));
customNumberFormats.put("weight",
new AliasTemplateNumberFormatFactory("0.##;; roundingMode=halfUp"));
cfg.setCustomNumberFormats(customNumberFormats);
Map<String, TemplateDateFormatFactory> customDateFormats
= new HashMap<String, TemplateDateFormatFactory>();
customDateFormats.put("fileDate", new AliasTemplateDateFormatFactory("dd/MMM/yy hh:mm a"));
customDateFormats.put("logEventTime", new AliasTemplateDateFormatFactory("iso ms u"));
cfg.setCustomDateFormats(customDateFormats);
现在,您可以在模板中执行此操作:
${product.price?string.@price}
${product.weight?string.@weight}
${lastModified?string.@fileDate}
${lastError.timestamp?string.@logEventTime}
请注意,AliasTemplateNumberFormatFactory
的构造函数参数自然也可以引用自定义格式:
Map<String, TemplateNumberFormatFactory> customNumberFormats
= new HashMap<String, TemplateNumberFormatFactory>();
customNumberFormats.put("base", BaseNTemplateNumberFormatFactory.INSTANCE);
customNumberFormats.put("oct", new AliasTemplateNumberFormatFactory("@base 8"));
cfg.setCustomNumberFormats(customNumberFormats);
因此,现在n?string.@oct
将数字格式化为八进制形式。
可识别模型的格式示例
在此示例中,我们指定了一种数字格式,如果将数字作为UnitAwareTemplateNumberModel
放入数据模型,则会自动在数字之后显示单位。首先让我们看看UnitAwareTemplateNumberModel
:
package com.example;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
public class UnitAwareTemplateNumberModel implements TemplateNumberModel {
private final Number value;
private final String unit;
public UnitAwareTemplateNumberModel(Number value, String unit) {
this.value = value;
this.unit = unit;
}
@Override
public Number getAsNumber() throws TemplateModelException {
return value;
}
public String getUnit() {
return unit;
}
}
当填充数据模型时,您可以执行以下操作:
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("weight", new UnitAwareTemplateNumberModel(1.5, "kg"));
// Rather than just: dataModel.put("weight", 1.5);
然后,如果我们在模板中包含以下内容:
${weight}
我们想看这个:
1.5 kg
为此,我们定义了以下自定义数字格式:
package com.example;
import java.util.Locale;
import freemarker.core.Environment;
import freemarker.core.TemplateNumberFormat;
import freemarker.core.TemplateNumberFormatFactory;
import freemarker.core.TemplateValueFormatException;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
/**
* A number format that takes any other number format as parameter (specified as a string, as
* usual in FreeMarker), then if the model is a {@link UnitAwareTemplateNumberModel}, it shows
* the unit after the number formatted with the other format, otherwise it just shows the formatted
* number without unit.
*/
public class UnitAwareTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
public static final UnitAwareTemplateNumberFormatFactory INSTANCE
= new UnitAwareTemplateNumberFormatFactory();
private UnitAwareTemplateNumberFormatFactory() {
// Defined to decrease visibility
}
@Override
public TemplateNumberFormat get(String params, Locale locale, Environment env)
throws TemplateValueFormatException {
return new UnitAwareNumberFormat(env.getTemplateNumberFormat(params, locale));
}
private static class UnitAwareNumberFormat extends TemplateNumberFormat {
private final TemplateNumberFormat innerFormat;
private UnitAwareNumberFormat(TemplateNumberFormat innerFormat) {
this.innerFormat = innerFormat;
}
@Override
public String formatToPlainText(TemplateNumberModel numberModel)
throws TemplateModelException, TemplateValueFormatException {
String innerResult = innerFormat.formatToPlainText(numberModel);
return numberModel instanceof UnitAwareTemplateNumberModel
? innerResult + " " + ((UnitAwareTemplateNumberModel) numberModel).getUnit()
: innerResult;
}
@Override
public boolean isLocaleBound() {
return innerFormat.isLocaleBound();
}
@Override
public String getDescription() {
return "unit-aware " + innerFormat.getDescription();
}
}
}
最后,我们将上述自定义格式设置为默认数字格式:
// Where you initalize the application-wide Configuration singleton:
Configuration cfg = ...;
Map<String, TemplateNumberFormatFactory> customNumberFormats = new HashMap<>();
customNumberFormats.put("ua", UnitAwareTemplateNumberFormatFactory.INSTANCE);
cfg.setCustomNumberFormats(customNumberFormats);
// Note: "0.####;; roundingMode=halfUp" is a standard format specified in FreeMarker.
cfg.setNumberFormat("@ua 0.####;; roundingMode=halfUp");