自定义数字和日期/时间格式

Page Contents

Overview

Note:

自 FreeMarker 2.3.24 起存在自定义格式(此处描述的那种格式)。

FreeMarker 允许您定义自己的数字和日期/时间/日期时间格式,并为它们关联一个名称。此机制有几个应用程序:

  • 自定义格式化程序算法:您可以使用自己的格式化程序算法,而不必依赖 FreeMarker 提供的算法。为此,实现freemarker.core.TemplateNumberFormatFactoryfreemarker.core.TemplateDateFormatFactory。您将找到有关below的一些示例。

  • 别名:您可以使用AliasTemplateNumberFormatFactoryAliasTemplateDateFormatFactory将应用程序特定的名称(如“价格”,“重量”,“ 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_formatscustom_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");