ICU4j如何通过NumberFormatter覆盖货币符号或添加自定义货币
问题
我需要自定义覆盖货币符号。举个例子,en locale下使用货币代码BGN时,无论我选择Narrow、Short还是VARIANT参数,ICU都只会返回BGN 0.67,但我期望得到лв 0.67。除此之外我们还有其他需要覆盖货币符号的场景。
com.ibm.icu.util.Currency没有提供任何可实现覆盖的set方法。Numberformatter在为数字追加货币符号时,会调用Currency.java中的getName方法获取货币符号:
public String getName(ULocale locale, int nameStyle, boolean[] isChoiceFormat) { ... CurrencyDisplayNames names = CurrencyDisplayNames.getInstance(locale); switch (nameStyle) { case SYMBOL_NAME: return names.getSymbol(subType); ... } ... }
CurrencyDisplayNames内置了预定义数据,也没有提供任何覆盖方法。
我发现Numberformat可以通过Numberformat.getCurrencyInstance()覆盖货币符号,但ICU建议新用户使用Numberformatter,请问我该如何实现符号覆盖,或者其他自定义需求比如在指定locale下将美元符号放在数字右侧?(ICU输出的大部分格式都符合我们的需求,我们仅需要针对某一locale下的特定货币做自定义调整)
解决方案
针对你的需求有两种轻量的实现方案,不需要修改ICU的底层源码:
方案1:自定义前后缀拼接(适合自定义规则少的场景)
核心思路是通过NumberFormatter的affixExclude参数排除默认的货币符号,拿到纯数字格式化结果后,再根据你预设的自定义规则拼接货币符号,调整前后位置:
import com.ibm.icu.number.CurrencyAffixExclude; import com.ibm.icu.number.NumberFormatter; import com.ibm.icu.util.Currency; import com.ibm.icu.util.ULocale; import java.util.HashMap; import java.util.Map; public class CustomCurrencyFormatter { // 自定义规则配置:key1=locale,key2=货币代码,值数组[前缀, 后缀] private static final Map<ULocale, Map<String, String[]>> CUSTOM_RULES = new HashMap<>(); static { // 示例配置:en locale下BGN前缀加лв,USD后缀加$ CUSTOM_RULES.put(ULocale.ENGLISH, Map.of( "BGN", new String[]{"лв ", ""}, "USD", new String[]{"", " $"} )); } public static String format(double amount, ULocale locale, String currencyCode) { Currency currency = Currency.getInstance(currencyCode); // 先判断是否有自定义规则 if (CUSTOM_RULES.containsKey(locale) && CUSTOM_RULES.get(locale).containsKey(currencyCode)) { String[] affix = CUSTOM_RULES.get(locale).get(currencyCode); // 获取不带货币符号的数字格式化结果 String formattedNum = NumberFormatter.forLocale(locale) .unit(currency) .affixExclude(CurrencyAffixExclude.CURRENCY) .format(amount) .toString(); return affix[0] + formattedNum + affix[1]; } // 无自定义规则直接返回默认格式化结果 return NumberFormatter.forLocale(locale) .unit(currency) .format(amount) .toString(); } public static void main(String[] args) { // 测试BGN的场景,输出:лв 0.67 System.out.println(format(0.67, ULocale.ENGLISH, "BGN")); // 测试美元后置场景,输出:123.45 $ System.out.println(format(123.45, ULocale.ENGLISH, "USD")); } }
方案2:SPI注册自定义CurrencyDisplayNames(适合自定义规则多、无侵入场景)
ICU支持通过SPI扩展CurrencyDisplayNamesProvider来覆盖默认的货币符号配置,原有NumberFormatter的调用代码不需要做任何修改就可以自动应用自定义规则:
- 自定义Provider实现类
import com.ibm.icu.util.CurrencyDisplayNames; import com.ibm.icu.util.ULocale; import com.ibm.icu.util.spi.CurrencyDisplayNamesProvider; import java.util.Collections; import java.util.Locale; import java.util.Set; public class CustomCurrencyDisplayNamesProvider extends CurrencyDisplayNamesProvider { @Override public CurrencyDisplayNames getInstance(ULocale locale) { // 只处理需要自定义的locale,其他返回null走默认逻辑 if (!ULocale.ENGLISH.equals(locale)) { return null; } return new CurrencyDisplayNames() { @Override public String getSymbol(String currencyCode) { // 自定义BGN的符号 if ("BGN".equals(currencyCode)) { return "лв"; } // 其他货币返回默认符号 return CurrencyDisplayNames.getInstance(locale, false).getSymbol(currencyCode); } // 其他方法直接代理默认实现即可 @Override public String getName(String currencyCode) { return CurrencyDisplayNames.getInstance(locale, false).getName(currencyCode); } @Override public String getPluralName(String currencyCode, String pluralKey) { return CurrencyDisplayNames.getInstance(locale, false).getPluralName(currencyCode, pluralKey); } @Override public Set<String> keySet() { return Collections.emptySet(); } @Override public ULocale getULocale() { return locale; } }; } @Override public Set<Locale> getAvailableLocales() { return Collections.singleton(ULocale.ENGLISH.toLocale()); } }
- 注册SPI:在项目的
META-INF/services目录下新建文件com.ibm.icu.util.spi.CurrencyDisplayNamesProvider,写入你的实现类全限定名(比如com.yourpackage.CustomCurrencyDisplayNamesProvider) - 原有代码直接调用NumberFormatter即可,输出会自动应用自定义符号。
内容的提问来源于stack exchange,提问作者sam

