/*
* Copyright 2014 Attila Szegedi, Daniel Dekany, Jonathan Revusky
*
* 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 freemarker.core;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import java.io.IOException;
import java.util.Locale;
import java.util.Map;
import org.junit.Before;
import org.junit.Test;
import com.google.common.collect.ImmutableMap;
import freemarker.template.Configuration;
import freemarker.template.SimpleNumber;
import freemarker.template.Template;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateDirectiveModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
import freemarker.test.TemplateTest;
@SuppressWarnings("boxing")
public class NumberFormatTest extends TemplateTest {
@Before
public void setup() {
Configuration cfg = getConfiguration();
cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_24);
cfg.setLocale(Locale.US);
cfg.setCustomNumberFormats(ImmutableMap.of(
"hex", HexTemplateNumberFormatFactory.INSTANCE,
"loc", LocaleSensitiveTemplateNumberFormatFactory.INSTANCE,
"base", BaseNTemplateNumberFormatFactory.INSTANCE));
}
@Test
public void testUnknownCustomFormat() throws Exception {
{
getConfiguration().setNumberFormat("@noSuchFormat");
Throwable exc = assertErrorContains("${1}", "\"@noSuchFormat\"", "\"noSuchFormat\"");
assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class));
}
{
getConfiguration().setNumberFormat("number");
Throwable exc = assertErrorContains("${1?string('@noSuchFormat2')}",
"\"@noSuchFormat2\"", "\"noSuchFormat2\"");
assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class));
}
}
@Test
public void testStringBI() throws Exception {
assertOutput("${11} ${11?string.@hex} ${12} ${12?string.@hex}", "11 b 12 c");
}
@Test
public void testSetting() throws Exception {
getConfiguration().setNumberFormat("@hex");
assertOutput("${11?string.number} ${11} ${12?string.number} ${12}", "11 b 12 c");
}
@Test
public void testSetting2() throws Exception {
assertOutput(
"<#setting numberFormat='@hex'>${11?string.number} ${11} ${12?string.number} ${12} ${13?string}"
+ "<#setting numberFormat='@loc'>${11?string.number} ${11} ${12?string.number} ${12} ${13?string}",
"11 b 12 c d"
+ "11 11_en_US 12 12_en_US 13_en_US");
}
@Test
public void testUnformattableNumber() throws Exception {
getConfiguration().setNumberFormat("@hex");
assertErrorContains("${1.1}", "hexadecimal int", "doesn't fit into an int");
}
@Test
public void testLocaleSensitive() throws Exception {
Configuration cfg = getConfiguration();
cfg.setNumberFormat("@loc");
assertOutput("${1.1}", "1.1_en_US");
cfg.setLocale(Locale.GERMANY);
assertOutput("${1.1}", "1.1_de_DE");
}
@Test
public void testLocaleSensitive2() throws Exception {
Configuration cfg = getConfiguration();
cfg.setNumberFormat("@loc");
assertOutput("${1.1} <#setting locale='de_DE'>${1.1}", "1.1_en_US 1.1_de_DE");
}
@Test
public void testCustomParameterized() throws Exception {
Configuration cfg = getConfiguration();
cfg.setNumberFormat("@base 2");
assertOutput("${11}", "1011");
assertOutput("${11?string}", "1011");
assertOutput("${11?string.@base_3}", "102");
assertErrorContains("${11?string.@base_xyz}", "\"@base_xyz\"", "\"xyz\"");
cfg.setNumberFormat("@base");
assertErrorContains("${11}", "\"@base\"", "format parameter is required");
}
@Test
public void testCustomWithFallback() throws Exception {
Configuration cfg = getConfiguration();
cfg.setNumberFormat("@base 2|0.0#");
assertOutput("${11}", "1011");
assertOutput("${11.34}", "11.34");
assertOutput("${11?string('@base 3|0.00')}", "102");
assertOutput("${11.2?string('@base 3|0.00')}", "11.20");
}
@Test
public void testExplicitLocale() throws Exception {
Template t = new Template(null, "", getConfiguration());
Environment env = t.createProcessingEnvironment(null, null);
TemplateNumberFormat defF = env.getTemplateNumberFormat();
//
TemplateNumberFormat explF = env.getTemplateNumberFormat("0.00");
assertEquals("1.25", explF.format(new SimpleNumber(1.25)));
//
TemplateNumberFormat expl2F = env.getTemplateNumberFormat("@loc");
assertEquals("1.25_en_US", expl2F.format(new SimpleNumber(1.25)));
TemplateNumberFormat explFFr = env.getTemplateNumberFormat("0.00", Locale.FRANCE);
assertNotSame(explF, explFFr);
assertEquals("1,25", explFFr.format(new SimpleNumber(1.25)));
//
TemplateNumberFormat expl2FFr = env.getTemplateNumberFormat("@loc", Locale.FRANCE);
assertEquals("1.25_fr_FR", expl2FFr.format(new SimpleNumber(1.25)));
assertSame(env.getTemplateNumberFormat(), defF);
//
assertSame(env.getTemplateNumberFormat("0.00"), explF);
//
assertSame(env.getTemplateNumberFormat("@loc"), expl2F);
}
/**
* ?string formats lazily (at least in 2.3.x), so it must make a snapshot of the format inputs when it's called.
*/
@Test
public void testStringBIDoesSnapshot() throws Exception {
// TemplateNumberModel-s shouldn't change, but we have to keep BC when that still happens.
final MutableTemplateNumberModel nm = new MutableTemplateNumberModel();
nm.setNumber(123);
addToDataModel("n", nm);
addToDataModel("incN", new TemplateDirectiveModel() {
public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
throws TemplateException, IOException {
nm.setNumber(nm.getAsNumber().intValue() + 1);
}
});
assertOutput(
"<#assign s1 = n?string>"
+ "<#setting numberFormat='@loc'>"
+ "<#assign s2 = n?string>"
+ "<#setting numberFormat='@hex'>"
+ "<#assign s3 = n?string>"
+ "${s1} ${s2} ${s3}",
"123 123_en_US 7b");
assertOutput(
"<#assign s1 = n?string>"
+ "<@incN />"
+ "<#assign s2 = n?string>"
+ "${s1} ${s2}",
"123 124");
}
@Test
public void testNullInModel() throws Exception {
addToDataModel("n", new MutableTemplateNumberModel());
assertErrorContains("${n}", "nothing inside it");
assertErrorContains("${n?string}", "nothing inside it");
}
@Test
public void testIcIAndEscaping() throws Exception {
Configuration cfg = getConfiguration();
cfg.setNumberFormat("@@0");
assertOutput("${10}", "@10");
cfg.setNumberFormat("@hex");
assertOutput("${10}", "a");
cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_23);
cfg.setNumberFormat("@@0");
assertOutput("${10}", "@@10");
cfg.setNumberFormat("@hex");
assertOutput("${10}", "@hex10");
}
private static class MutableTemplateNumberModel implements TemplateNumberModel {
private Number number;
public void setNumber(Number number) {
this.number = number;
}
public Number getAsNumber() throws TemplateModelException {
return number;
}
}
}