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

Page Contents

Overview

Note:

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

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

可以使用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");
上一章 首页 下一章