/* * Copyright 2012 Google Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.google.template.soy.i18ndirectives; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; import com.google.template.soy.data.SoyValue; import com.google.template.soy.data.restricted.NumberData; import com.google.template.soy.data.restricted.StringData; import com.google.template.soy.internal.targetexpr.TargetExpr; import com.google.template.soy.jssrc.restricted.JsExpr; import com.google.template.soy.jssrc.restricted.SoyLibraryAssistedJsSrcPrintDirective; import com.google.template.soy.pysrc.restricted.PyExpr; import com.google.template.soy.pysrc.restricted.PyExprUtils; import com.google.template.soy.pysrc.restricted.PyFunctionExprBuilder; import com.google.template.soy.pysrc.restricted.SoyPySrcPrintDirective; import com.google.template.soy.shared.restricted.ApiCallScopeBindingAnnotations.LocaleString; import com.google.template.soy.shared.restricted.SoyJavaPrintDirective; import com.ibm.icu.text.CompactDecimalFormat; import com.ibm.icu.text.CompactDecimalFormat.CompactStyle; import com.ibm.icu.text.NumberFormat; import com.ibm.icu.util.ULocale; import java.util.List; import java.util.Set; import javax.inject.Inject; import javax.inject.Provider; /** * A directive that formats an input number based on Locale of the current SoyMsgBundle. It may take * two optional arguments. The first is a lower-case string describing the type of format to apply, * which can be one of 'decimal', 'currency', 'percent', 'scientific', 'compact_short', or * 'compact_long'. If this argument is not provided, the default 'decimal' will be used. The second * argument is the "numbers" keyword passed to the ICU4J's locale. For instance, it can be "native" * so that we show native characters in languages like arabic (this argument is ignored for * templates running in JavaScript). * * <p>Usage examples: {@code {$value|formatNum} {$value|formatNum:'decimal'} * {$value|formatNum:'decimal','native'} } * */ class FormatNumDirective implements SoyJavaPrintDirective, SoyLibraryAssistedJsSrcPrintDirective, SoyPySrcPrintDirective { // Map of format arguments to the Closure Format enum. private static final ImmutableMap<String, String> JS_ARGS_TO_ENUM = ImmutableMap.<String, String>builder() .put("'decimal'", "goog.i18n.NumberFormat.Format.DECIMAL") .put("'currency'", "goog.i18n.NumberFormat.Format.CURRENCY") .put("'percent'", "goog.i18n.NumberFormat.Format.PERCENT") .put("'scientific'", "goog.i18n.NumberFormat.Format.SCIENTIFIC") .put("'compact_short'", "goog.i18n.NumberFormat.Format.COMPACT_SHORT") .put("'compact_long'", "goog.i18n.NumberFormat.Format.COMPACT_LONG") .build(); // This directive can be called with no arguments, with one argument setting the format type, // or with two arguments setting the format type and the 'numbers' keyword for the ICU4J // formatter. private static final ImmutableSet<Integer> VALID_ARGS_SIZES = ImmutableSet.of(0, 1, 2); private static final ImmutableSet<String> REQUIRED_JS_LIBS = ImmutableSet.of("goog.i18n.NumberFormat"); private static final String DEFAULT_FORMAT = "decimal"; /** * Provide the current Locale string. * * <p>Note that this Locale value is only used in the Java environment. Closure does not provide a * clear mechanism to override the NumberFormat defined when the NumberFormat module loads. This * is probably not a significant loss of functionality, since the primary reason to inject the * LocaleString is because the Java VM's default Locale may not be the same as the desired Locale * for the page, while in the JavaScript environment, the value of goog.LOCALE should reliably * indicate which Locale Soy should use. Similarly, the Python backend relies on implementation * specific runtime locale support. */ private final Provider<String> localeStringProvider; @Inject FormatNumDirective(@LocaleString Provider<String> localeStringProvider) { this.localeStringProvider = localeStringProvider; } @Override public String getName() { return "|formatNum"; } @Override public Set<Integer> getValidArgsSizes() { return VALID_ARGS_SIZES; } @Override public boolean shouldCancelAutoescape() { return false; } @Override public SoyValue applyForJava(SoyValue value, List<SoyValue> args) { ULocale uLocale = I18nUtils.parseULocale(localeStringProvider.get()).setKeywordValue("numbers", "local"); if (args.size() > 1) { // A keyword for ULocale was passed (like 'native', for instance, to use native characters). uLocale = uLocale.setKeywordValue("numbers", args.get(1).stringValue()); } NumberFormat numberFormat; String formatType = args.isEmpty() ? DEFAULT_FORMAT : args.get(0).stringValue(); if ("decimal".equals(formatType)) { numberFormat = NumberFormat.getInstance(uLocale); } else if ("percent".equals(formatType)) { numberFormat = NumberFormat.getPercentInstance(uLocale); } else if ("currency".equals(formatType)) { numberFormat = NumberFormat.getCurrencyInstance(uLocale); } else if ("scientific".equals(formatType)) { numberFormat = NumberFormat.getScientificInstance(uLocale); } else if ("compact_short".equals(formatType)) { CompactDecimalFormat compactNumberFormat = CompactDecimalFormat.getInstance(uLocale, CompactStyle.SHORT); compactNumberFormat.setMaximumSignificantDigits(3); numberFormat = compactNumberFormat; } else if ("compact_long".equals(formatType)) { CompactDecimalFormat compactNumberFormat = CompactDecimalFormat.getInstance(uLocale, CompactStyle.LONG); compactNumberFormat.setMaximumSignificantDigits(3); numberFormat = compactNumberFormat; } else { throw new IllegalArgumentException( "First argument to formatNum must be " + "constant, and one of: 'decimal', 'currency', 'percent', 'scientific', " + "'compact_short', or 'compact_long'."); } return StringData.forValue(numberFormat.format(((NumberData) value).toFloat())); } @Override public JsExpr applyForJsSrc(JsExpr value, List<JsExpr> args) { String numberFormatType = parseFormat(args); StringBuilder expr = new StringBuilder(); expr.append("(new goog.i18n.NumberFormat(" + JS_ARGS_TO_ENUM.get(numberFormatType) + "))"); if ("'compact_short'".equals(numberFormatType) || "'compact_long'".equals(numberFormatType)) { expr.append(".setSignificantDigits(3)"); } expr.append(".format(" + value.getText() + ")"); return new JsExpr(expr.toString(), Integer.MAX_VALUE); } @Override public PyExpr applyForPySrc(PyExpr value, List<PyExpr> args) { String numberFormatType = parseFormat(args); PyFunctionExprBuilder builder = new PyFunctionExprBuilder(PyExprUtils.TRANSLATOR_NAME + ".format_num") .addArg(value) .addArg(new PyExpr(numberFormatType, Integer.MAX_VALUE)); return builder.asPyStringExpr(); } @Override public ImmutableSet<String> getRequiredJsLibNames() { return REQUIRED_JS_LIBS; } /** * Validates that the provided format matches a supported format, and returns the value, if not, * this throws an exception. * * @param args The list of provided arguments. * @return String The number format type. */ private static String parseFormat(List<? extends TargetExpr> args) { String numberFormatType = !args.isEmpty() ? args.get(0).getText() : "'" + DEFAULT_FORMAT + "'"; if (!JS_ARGS_TO_ENUM.containsKey(numberFormatType)) { String validKeys = Joiner.on("', '").join(JS_ARGS_TO_ENUM.keySet()); throw new IllegalArgumentException( "First argument to formatNum must be " + "constant, and one of: '" + validKeys + "'."); } return numberFormatType; } }