diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java index 544230d9c..216a1d2bd 100644 --- a/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FilterLibrary.java @@ -62,6 +62,7 @@ protected void registerDefaults() { FormatFilter.class, FormatDateFilter.class, FormatDatetimeFilter.class, + FormatNumberFilter.class, FormatTimeFilter.class, FromJsonFilter.class, FromYamlFilter.class, diff --git a/src/main/java/com/hubspot/jinjava/lib/filter/FormatNumberFilter.java b/src/main/java/com/hubspot/jinjava/lib/filter/FormatNumberFilter.java new file mode 100644 index 000000000..924e8e325 --- /dev/null +++ b/src/main/java/com/hubspot/jinjava/lib/filter/FormatNumberFilter.java @@ -0,0 +1,114 @@ +package com.hubspot.jinjava.lib.filter; + +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableMap; +import com.hubspot.jinjava.doc.annotations.JinjavaDoc; +import com.hubspot.jinjava.doc.annotations.JinjavaParam; +import com.hubspot.jinjava.doc.annotations.JinjavaSnippet; +import com.hubspot.jinjava.interpret.JinjavaInterpreter; +import com.hubspot.jinjava.interpret.TemplateError; +import com.hubspot.jinjava.interpret.TemplateError.ErrorItem; +import com.hubspot.jinjava.interpret.TemplateError.ErrorReason; +import com.hubspot.jinjava.interpret.TemplateError.ErrorType; +import java.math.BigDecimal; +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; + +@JinjavaDoc( + value = "Formats a given number based on the locale passed in as a parameter.", + input = @JinjavaParam( + value = "value", + desc = "The number to be formatted based on locale", + required = true + ), + params = { + @JinjavaParam( + value = "locale", + desc = "Locale in which to format the number. The default is the page's locale." + ), + @JinjavaParam( + value = "max decimal precision", + type = "number", + desc = "A number input that determines the decimal precision of the formatted value. If the number of decimal digits from the input value is less than the decimal precision number, use the number of decimal digits from the input value. Otherwise, use the decimal precision number. The default is the number of decimal digits from the input value." + ) + }, + snippets = { + @JinjavaSnippet(code = "{{ number|format_number }}"), + @JinjavaSnippet(code = "{{ number|format_number(\"en-US\") }}"), + @JinjavaSnippet(code = "{{ number|format_number(\"en-US\", 3) }}") + } +) +public class FormatNumberFilter implements Filter { + private static final String FORMAT_NUMBER_FILTER_NAME = "format_number"; + + @Override + public String getName() { + return FORMAT_NUMBER_FILTER_NAME; + } + + @Override + public Object filter(Object var, JinjavaInterpreter interpreter, String... args) { + Locale locale = args.length > 0 && !Strings.isNullOrEmpty(args[0]) + ? Locale.forLanguageTag(args[0]) + : interpreter.getConfig().getLocale(); + + BigDecimal number; + try { + number = parseInput(var); + } catch (Exception e) { + if (interpreter.getContext().isValidationMode()) { + return ""; + } + interpreter.addError( + new TemplateError( + ErrorType.WARNING, + ErrorReason.INVALID_INPUT, + ErrorItem.FILTER, + "Input value '" + var + "' could not be parsed.", + null, + interpreter.getLineNumber(), + e, + null, + ImmutableMap.of("value", Objects.toString(var)) + ) + ); + return var; + } + + Optional maxDecimalPrecision = args.length > 1 + ? Optional.of(Integer.parseInt(args[1])) + : Optional.empty(); + + return formatNumber(locale, number, maxDecimalPrecision); + } + + private BigDecimal parseInput(Object input) throws Exception { + DecimalFormat df = (DecimalFormat) NumberFormat.getInstance(); + df.setParseBigDecimal(true); + + return (BigDecimal) df.parseObject(Objects.toString(input)); + } + + private String formatNumber( + Locale locale, + BigDecimal number, + Optional maxDecimalPrecision + ) { + NumberFormat numberFormat = NumberFormat.getNumberInstance(locale); + int numDecimalPlacesInInput = Math.max(0, number.scale()); + + numberFormat.setMaximumFractionDigits( + Math.min( + numDecimalPlacesInInput, + maxDecimalPrecision.isPresent() + ? maxDecimalPrecision.get() + : numDecimalPlacesInInput + ) + ); + + return numberFormat.format(number); + } +} diff --git a/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java b/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java new file mode 100644 index 000000000..25b715a0a --- /dev/null +++ b/src/test/java/com/hubspot/jinjava/lib/filter/FormatNumberFilterTest.java @@ -0,0 +1,74 @@ +package com.hubspot.jinjava.lib.filter; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.hubspot.jinjava.BaseJinjavaTest; +import java.util.HashMap; +import org.junit.Before; +import org.junit.Test; + +public class FormatNumberFilterTest extends BaseJinjavaTest { + + @Before + public void setup() {} + + @Test + public void testFormatNumberFilter() { + assertThat( + jinjava.render("{{1000|format_number('en-US')}}", new HashMap()) + ) + .isEqualTo("1,000"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('en-US') }}", + new HashMap() + ) + ) + .isEqualTo("1,000.333"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('en-US', 2) }}", + new HashMap() + ) + ) + .isEqualTo("1,000.33"); + + assertThat( + jinjava.render("{{ 1000|format_number('fr') }}", new HashMap()) + ) + .isEqualTo("1\u00a0000"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('fr') }}", + new HashMap() + ) + ) + .isEqualTo("1\u00a0000,333"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('fr', 2) }}", + new HashMap() + ) + ) + .isEqualTo("1\u00a0000,33"); + + assertThat( + jinjava.render("{{ 1000|format_number('es') }}", new HashMap()) + ) + .isEqualTo("1.000"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('es') }}", + new HashMap() + ) + ) + .isEqualTo("1.000,333"); + assertThat( + jinjava.render( + "{{ 1000.333|format_number('es', 2) }}", + new HashMap() + ) + ) + .isEqualTo("1.000,33"); + } +}