ArithmeticEngine related classes were moved to org.apache.freemarker.core.arithmetic.
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/dcb0e063 Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/dcb0e063 Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/dcb0e063 Branch: refs/heads/3 Commit: dcb0e0634aeabc0221c9e89126f819c3aa60560d Parents: 7308532 Author: ddekany <ddek...@apache.org> Authored: Sat Feb 25 08:13:52 2017 +0100 Committer: ddekany <ddek...@apache.org> Committed: Sat Feb 25 08:13:52 2017 +0100 ---------------------------------------------------------------------- .../freemarker/core/ASTExpAddOrConcat.java | 1 + .../freemarker/core/ASTExpNegateOrPlus.java | 5 +- .../freemarker/core/ArithmeticEngine.java | 549 ------------------- .../freemarker/core/ArithmeticExpression.java | 1 + .../freemarker/core/BuiltInsForSequences.java | 1 + .../apache/freemarker/core/Configurable.java | 17 +- .../freemarker/core/ParserConfiguration.java | 1 + .../freemarker/core/TemplateConfiguration.java | 1 + .../org/apache/freemarker/core/_EvalUtil.java | 4 +- ..._ParserConfigurationWithInheritedFormat.java | 1 + .../core/arithmetic/ArithmeticEngine.java | 93 ++++ .../impl/BigDecimalArithmeticEngine.java | 107 ++++ .../impl/ConservativeArithmeticEngine.java | 381 +++++++++++++ .../core/model/TemplateNumberModel.java | 2 +- .../freemarker/core/util/_NumberUtil.java | 23 + .../core/valueformat/TemplateNumberFormat.java | 2 +- src/manual/en_US/FM3-CHANGE-LOG.txt | 1 + .../core/ObjectBuilderSettingsTest.java | 4 +- .../core/TemplateConfigurationTest.java | 4 +- 19 files changed, 634 insertions(+), 564 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java b/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java index d37fa14..48a1fa3 100644 --- a/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java +++ b/src/main/java/org/apache/freemarker/core/ASTExpAddOrConcat.java @@ -22,6 +22,7 @@ package org.apache.freemarker.core; import java.util.HashSet; import java.util.Set; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; import org.apache.freemarker.core.model.TemplateCollectionModel; import org.apache.freemarker.core.model.TemplateHashModel; import org.apache.freemarker.core.model.TemplateHashModelEx; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java b/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java index f007442..233d89d 100644 --- a/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java +++ b/src/main/java/org/apache/freemarker/core/ASTExpNegateOrPlus.java @@ -19,6 +19,8 @@ package org.apache.freemarker.core; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.arithmetic.impl.ConservativeArithmeticEngine; import org.apache.freemarker.core.model.TemplateModel; import org.apache.freemarker.core.model.TemplateNumberModel; import org.apache.freemarker.core.model.impl.SimpleNumber; @@ -54,7 +56,8 @@ final class ASTExpNegateOrPlus extends ASTExpression { } target.assertNonNull(targetModel, env); Number n = targetModel.getAsNumber(); - n = ArithmeticEngine.CONSERVATIVE_ENGINE.multiply(MINUS_ONE, n); + // [FM3] Add ArithmeticEngine.negate, then use the engine from the env + n = ConservativeArithmeticEngine.INSTANCE.multiply(MINUS_ONE, n); return new SimpleNumber(n); } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/ArithmeticEngine.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ArithmeticEngine.java b/src/main/java/org/apache/freemarker/core/ArithmeticEngine.java deleted file mode 100644 index b3ffe02..0000000 --- a/src/main/java/org/apache/freemarker/core/ArithmeticEngine.java +++ /dev/null @@ -1,549 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 org.apache.freemarker.core; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.HashMap; -import java.util.Map; - -import org.apache.freemarker.core.util.BugException; -import org.apache.freemarker.core.util._NumberUtil; -import org.apache.freemarker.core.util._StringUtil; - -/** - * Class to perform arithmetic operations. - */ - -public abstract class ArithmeticEngine { - - /** - * Arithmetic engine that converts all numbers to {@link BigDecimal} and - * then operates on them. This is FreeMarker's default arithmetic engine. - */ - public static final BigDecimalEngine BIGDECIMAL_ENGINE = new BigDecimalEngine(); - /** - * Arithmetic engine that uses (more-or-less) the widening conversions of - * Java language to determine the type of result of operation, instead of - * converting everything to BigDecimal up front. - */ - public static final ConservativeEngine CONSERVATIVE_ENGINE = new ConservativeEngine(); - - public abstract int compareNumbers(Number first, Number second) throws TemplateException; - public abstract Number add(Number first, Number second) throws TemplateException; - public abstract Number subtract(Number first, Number second) throws TemplateException; - public abstract Number multiply(Number first, Number second) throws TemplateException; - public abstract Number divide(Number first, Number second) throws TemplateException; - public abstract Number modulus(Number first, Number second) throws TemplateException; - - /** - * Should be able to parse all FTL numerical literals, Java Double toString results, and XML Schema numbers. - * This means these should be parsed successfully, except if the arithmetical engine - * couldn't support the resulting value anyway (such as NaN, infinite, even non-integers): - * {@code -123.45}, {@code 1.5e3}, {@code 1.5E3}, {@code 0005}, {@code +0}, {@code -0}, {@code NaN}, - * {@code INF}, {@code -INF}, {@code Infinity}, {@code -Infinity}. - */ - public abstract Number toNumber(String s); - - protected int minScale = 12; - protected int maxScale = 12; - protected int roundingPolicy = BigDecimal.ROUND_HALF_UP; - - /** - * Sets the minimal scale to use when dividing BigDecimal numbers. Default - * value is 12. - */ - public void setMinScale(int minScale) { - if (minScale < 0) { - throw new IllegalArgumentException("minScale < 0"); - } - this.minScale = minScale; - } - - /** - * Sets the maximal scale to use when multiplying BigDecimal numbers. - * Default value is 100. - */ - public void setMaxScale(int maxScale) { - if (maxScale < minScale) { - throw new IllegalArgumentException("maxScale < minScale"); - } - this.maxScale = maxScale; - } - - public void setRoundingPolicy(int roundingPolicy) { - if (roundingPolicy != BigDecimal.ROUND_CEILING - && roundingPolicy != BigDecimal.ROUND_DOWN - && roundingPolicy != BigDecimal.ROUND_FLOOR - && roundingPolicy != BigDecimal.ROUND_HALF_DOWN - && roundingPolicy != BigDecimal.ROUND_HALF_EVEN - && roundingPolicy != BigDecimal.ROUND_HALF_UP - && roundingPolicy != BigDecimal.ROUND_UNNECESSARY - && roundingPolicy != BigDecimal.ROUND_UP) { - throw new IllegalArgumentException("invalid rounding policy"); - } - - this.roundingPolicy = roundingPolicy; - } - - /** - * This is the default arithmetic engine in FreeMarker. It converts every - * number it receives into {@link BigDecimal}, then operates on these - * converted {@link BigDecimal}s. - */ - public static class BigDecimalEngine - extends - ArithmeticEngine { - @Override - public int compareNumbers(Number first, Number second) { - // We try to find the result based on the sign (+/-/0) first, because: - // - It's much faster than converting to BigDecial, and comparing to 0 is the most common comparison. - // - It doesn't require any type conversions, and thus things like "Infinity > 0" won't fail. - int firstSignum = _NumberUtil.getSignum(first); - int secondSignum = _NumberUtil.getSignum(second); - if (firstSignum != secondSignum) { - return firstSignum < secondSignum ? -1 : (firstSignum > secondSignum ? 1 : 0); - } else if (firstSignum == 0 && secondSignum == 0) { - return 0; - } else { - BigDecimal left = toBigDecimal(first); - BigDecimal right = toBigDecimal(second); - return left.compareTo(right); - } - } - - @Override - public Number add(Number first, Number second) { - BigDecimal left = toBigDecimal(first); - BigDecimal right = toBigDecimal(second); - return left.add(right); - } - - @Override - public Number subtract(Number first, Number second) { - BigDecimal left = toBigDecimal(first); - BigDecimal right = toBigDecimal(second); - return left.subtract(right); - } - - @Override - public Number multiply(Number first, Number second) { - BigDecimal left = toBigDecimal(first); - BigDecimal right = toBigDecimal(second); - BigDecimal result = left.multiply(right); - if (result.scale() > maxScale) { - result = result.setScale(maxScale, roundingPolicy); - } - return result; - } - - @Override - public Number divide(Number first, Number second) { - BigDecimal left = toBigDecimal(first); - BigDecimal right = toBigDecimal(second); - return divide(left, right); - } - - @Override - public Number modulus(Number first, Number second) { - long left = first.longValue(); - long right = second.longValue(); - return Long.valueOf(left % right); - } - - @Override - public Number toNumber(String s) { - return toBigDecimalOrDouble(s); - } - - private BigDecimal divide(BigDecimal left, BigDecimal right) { - int scale1 = left.scale(); - int scale2 = right.scale(); - int scale = Math.max(scale1, scale2); - scale = Math.max(minScale, scale); - return left.divide(right, scale, roundingPolicy); - } - } - - /** - * An arithmetic engine that conservatively widens the operation arguments - * to extent that they can hold the result of the operation. Widening - * conversions occur in following situations: - * <ul> - * <li>byte and short are always widened to int (alike to Java language).</li> - * <li>To preserve magnitude: when operands are of different types, the - * result type is the type of the wider operand.</li> - * <li>to avoid overflows: if add, subtract, or multiply would overflow on - * integer types, the result is widened from int to long, or from long to - * BigInteger.</li> - * <li>to preserve fractional part: if a division of integer types would - * have a fractional part, int and long are converted to double, and - * BigInteger is converted to BigDecimal. An operation on a float and a - * long results in a double. An operation on a float or double and a - * BigInteger results in a BigDecimal.</li> - * </ul> - */ - public static class ConservativeEngine extends ArithmeticEngine { - private static final int INTEGER = 0; - private static final int LONG = 1; - private static final int FLOAT = 2; - private static final int DOUBLE = 3; - private static final int BIGINTEGER = 4; - private static final int BIGDECIMAL = 5; - - private static final Map classCodes = createClassCodesMap(); - - @Override - public int compareNumbers(Number first, Number second) throws TemplateException { - switch(getCommonClassCode(first, second)) { - case INTEGER: { - int n1 = first.intValue(); - int n2 = second.intValue(); - return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); - } - case LONG: { - long n1 = first.longValue(); - long n2 = second.longValue(); - return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); - } - case FLOAT: { - float n1 = first.floatValue(); - float n2 = second.floatValue(); - return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); - } - case DOUBLE: { - double n1 = first.doubleValue(); - double n2 = second.doubleValue(); - return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); - } - case BIGINTEGER: { - BigInteger n1 = toBigInteger(first); - BigInteger n2 = toBigInteger(second); - return n1.compareTo(n2); - } - case BIGDECIMAL: { - BigDecimal n1 = toBigDecimal(first); - BigDecimal n2 = toBigDecimal(second); - return n1.compareTo(n2); - } - } - // Make the compiler happy. getCommonClassCode() is guaranteed to - // return only above codes, or throw an exception. - throw new Error(); - } - - @Override - public Number add(Number first, Number second) throws TemplateException { - switch(getCommonClassCode(first, second)) { - case INTEGER: { - int n1 = first.intValue(); - int n2 = second.intValue(); - int n = n1 + n2; - return - ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check - ? Long.valueOf(((long) n1) + n2) - : Integer.valueOf(n); - } - case LONG: { - long n1 = first.longValue(); - long n2 = second.longValue(); - long n = n1 + n2; - return - ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check - ? toBigInteger(first).add(toBigInteger(second)) - : Long.valueOf(n); - } - case FLOAT: { - return Float.valueOf(first.floatValue() + second.floatValue()); - } - case DOUBLE: { - return Double.valueOf(first.doubleValue() + second.doubleValue()); - } - case BIGINTEGER: { - BigInteger n1 = toBigInteger(first); - BigInteger n2 = toBigInteger(second); - return n1.add(n2); - } - case BIGDECIMAL: { - BigDecimal n1 = toBigDecimal(first); - BigDecimal n2 = toBigDecimal(second); - return n1.add(n2); - } - } - // Make the compiler happy. getCommonClassCode() is guaranteed to - // return only above codes, or throw an exception. - throw new Error(); - } - - @Override - public Number subtract(Number first, Number second) throws TemplateException { - switch(getCommonClassCode(first, second)) { - case INTEGER: { - int n1 = first.intValue(); - int n2 = second.intValue(); - int n = n1 - n2; - return - ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check - ? Long.valueOf(((long) n1) - n2) - : Integer.valueOf(n); - } - case LONG: { - long n1 = first.longValue(); - long n2 = second.longValue(); - long n = n1 - n2; - return - ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check - ? toBigInteger(first).subtract(toBigInteger(second)) - : Long.valueOf(n); - } - case FLOAT: { - return Float.valueOf(first.floatValue() - second.floatValue()); - } - case DOUBLE: { - return Double.valueOf(first.doubleValue() - second.doubleValue()); - } - case BIGINTEGER: { - BigInteger n1 = toBigInteger(first); - BigInteger n2 = toBigInteger(second); - return n1.subtract(n2); - } - case BIGDECIMAL: { - BigDecimal n1 = toBigDecimal(first); - BigDecimal n2 = toBigDecimal(second); - return n1.subtract(n2); - } - } - // Make the compiler happy. getCommonClassCode() is guaranteed to - // return only above codes, or throw an exception. - throw new Error(); - } - - @Override - public Number multiply(Number first, Number second) throws TemplateException { - switch(getCommonClassCode(first, second)) { - case INTEGER: { - int n1 = first.intValue(); - int n2 = second.intValue(); - int n = n1 * n2; - return - n1 == 0 || n / n1 == n2 // overflow check - ? Integer.valueOf(n) - : Long.valueOf(((long) n1) * n2); - } - case LONG: { - long n1 = first.longValue(); - long n2 = second.longValue(); - long n = n1 * n2; - return - n1 == 0L || n / n1 == n2 // overflow check - ? Long.valueOf(n) - : toBigInteger(first).multiply(toBigInteger(second)); - } - case FLOAT: { - return Float.valueOf(first.floatValue() * second.floatValue()); - } - case DOUBLE: { - return Double.valueOf(first.doubleValue() * second.doubleValue()); - } - case BIGINTEGER: { - BigInteger n1 = toBigInteger(first); - BigInteger n2 = toBigInteger(second); - return n1.multiply(n2); - } - case BIGDECIMAL: { - BigDecimal n1 = toBigDecimal(first); - BigDecimal n2 = toBigDecimal(second); - BigDecimal r = n1.multiply(n2); - return r.scale() > maxScale ? r.setScale(maxScale, roundingPolicy) : r; - } - } - // Make the compiler happy. getCommonClassCode() is guaranteed to - // return only above codes, or throw an exception. - throw new Error(); - } - - @Override - public Number divide(Number first, Number second) throws TemplateException { - switch(getCommonClassCode(first, second)) { - case INTEGER: { - int n1 = first.intValue(); - int n2 = second.intValue(); - if (n1 % n2 == 0) { - return Integer.valueOf(n1 / n2); - } - return Double.valueOf(((double) n1) / n2); - } - case LONG: { - long n1 = first.longValue(); - long n2 = second.longValue(); - if (n1 % n2 == 0) { - return Long.valueOf(n1 / n2); - } - return Double.valueOf(((double) n1) / n2); - } - case FLOAT: { - return Float.valueOf(first.floatValue() / second.floatValue()); - } - case DOUBLE: { - return Double.valueOf(first.doubleValue() / second.doubleValue()); - } - case BIGINTEGER: { - BigInteger n1 = toBigInteger(first); - BigInteger n2 = toBigInteger(second); - BigInteger[] divmod = n1.divideAndRemainder(n2); - if (divmod[1].equals(BigInteger.ZERO)) { - return divmod[0]; - } else { - BigDecimal bd1 = new BigDecimal(n1); - BigDecimal bd2 = new BigDecimal(n2); - return bd1.divide(bd2, minScale, roundingPolicy); - } - } - case BIGDECIMAL: { - BigDecimal n1 = toBigDecimal(first); - BigDecimal n2 = toBigDecimal(second); - int scale1 = n1.scale(); - int scale2 = n2.scale(); - int scale = Math.max(scale1, scale2); - scale = Math.max(minScale, scale); - return n1.divide(n2, scale, roundingPolicy); - } - } - // Make the compiler happy. getCommonClassCode() is guaranteed to - // return only above codes, or throw an exception. - throw new Error(); - } - - @Override - public Number modulus(Number first, Number second) throws TemplateException { - switch(getCommonClassCode(first, second)) { - case INTEGER: { - return Integer.valueOf(first.intValue() % second.intValue()); - } - case LONG: { - return Long.valueOf(first.longValue() % second.longValue()); - } - case FLOAT: { - return Float.valueOf(first.floatValue() % second.floatValue()); - } - case DOUBLE: { - return Double.valueOf(first.doubleValue() % second.doubleValue()); - } - case BIGINTEGER: { - BigInteger n1 = toBigInteger(first); - BigInteger n2 = toBigInteger(second); - return n1.mod(n2); - } - case BIGDECIMAL: { - throw new _MiscTemplateException("Can't calculate remainder on BigDecimals"); - } - } - // Make the compiler happy. getCommonClassCode() is guaranteed to - // return only above codes, or throw an exception. - throw new BugException(); - } - - @Override - public Number toNumber(String s) { - Number n = toBigDecimalOrDouble(s); - return n instanceof BigDecimal ? _NumberUtil.optimizeNumberRepresentation(n) : n; - } - - private static Map createClassCodesMap() { - Map map = new HashMap(17); - Integer intcode = Integer.valueOf(INTEGER); - map.put(Byte.class, intcode); - map.put(Short.class, intcode); - map.put(Integer.class, intcode); - map.put(Long.class, Integer.valueOf(LONG)); - map.put(Float.class, Integer.valueOf(FLOAT)); - map.put(Double.class, Integer.valueOf(DOUBLE)); - map.put(BigInteger.class, Integer.valueOf(BIGINTEGER)); - map.put(BigDecimal.class, Integer.valueOf(BIGDECIMAL)); - return map; - } - - private static int getClassCode(Number num) throws TemplateException { - try { - return ((Integer) classCodes.get(num.getClass())).intValue(); - } catch (NullPointerException e) { - if (num == null) { - throw new _MiscTemplateException("The Number object was null."); - } else { - throw new _MiscTemplateException("Unknown number type ", num.getClass().getName()); - } - } - } - - private static int getCommonClassCode(Number num1, Number num2) throws TemplateException { - int c1 = getClassCode(num1); - int c2 = getClassCode(num2); - int c = c1 > c2 ? c1 : c2; - // If BigInteger is combined with a Float or Double, the result is a - // BigDecimal instead of BigInteger in order not to lose the - // fractional parts. If Float is combined with Long, the result is a - // Double instead of Float to preserve the bigger bit width. - switch(c) { - case FLOAT: { - if ((c1 < c2 ? c1 : c2) == LONG) { - return DOUBLE; - } - break; - } - case BIGINTEGER: { - int min = c1 < c2 ? c1 : c2; - if (min == DOUBLE || min == FLOAT) { - return BIGDECIMAL; - } - break; - } - } - return c; - } - - private static BigInteger toBigInteger(Number num) { - return num instanceof BigInteger ? (BigInteger) num : new BigInteger(num.toString()); - } - } - - private static BigDecimal toBigDecimal(Number num) { - try { - return num instanceof BigDecimal ? (BigDecimal) num : new BigDecimal(num.toString()); - } catch (NumberFormatException e) { - // The exception message is useless, so we add a new one: - throw new NumberFormatException("Can't parse this as BigDecimal number: " + _StringUtil.jQuote(num)); - } - } - - private static Number toBigDecimalOrDouble(String s) { - if (s.length() > 2) { - char c = s.charAt(0); - if (c == 'I' && (s.equals("INF") || s.equals("Infinity"))) { - return Double.valueOf(Double.POSITIVE_INFINITY); - } else if (c == 'N' && s.equals("NaN")) { - return Double.valueOf(Double.NaN); - } else if (c == '-' && s.charAt(1) == 'I' && (s.equals("-INF") || s.equals("-Infinity"))) { - return Double.valueOf(Double.NEGATIVE_INFINITY); - } - } - return new BigDecimal(s); - } - -} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java b/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java index d075e45..6ceb91a 100644 --- a/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java +++ b/src/main/java/org/apache/freemarker/core/ArithmeticExpression.java @@ -19,6 +19,7 @@ package org.apache.freemarker.core; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; import org.apache.freemarker.core.model.TemplateModel; import org.apache.freemarker.core.model.impl.SimpleNumber; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java b/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java index 793eb1e..92c59b8 100644 --- a/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java +++ b/src/main/java/org/apache/freemarker/core/BuiltInsForSequences.java @@ -27,6 +27,7 @@ import java.util.Comparator; import java.util.Date; import java.util.List; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; import org.apache.freemarker.core.model.Constants; import org.apache.freemarker.core.model.TemplateBooleanModel; import org.apache.freemarker.core.model.TemplateCollectionModel; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/Configurable.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/Configurable.java b/src/main/java/org/apache/freemarker/core/Configurable.java index 52fa6d2..000f4bb 100644 --- a/src/main/java/org/apache/freemarker/core/Configurable.java +++ b/src/main/java/org/apache/freemarker/core/Configurable.java @@ -39,6 +39,9 @@ import java.util.Properties; import java.util.Set; import java.util.TimeZone; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.arithmetic.impl.BigDecimalArithmeticEngine; +import org.apache.freemarker.core.arithmetic.impl.ConservativeArithmeticEngine; import org.apache.freemarker.core.model.ObjectWrapper; import org.apache.freemarker.core.model.TemplateModel; import org.apache.freemarker.core.model.impl.DefaultObjectWrapper; @@ -64,11 +67,7 @@ import org.apache.freemarker.core.templateresolver.PathRegexMatcher; import org.apache.freemarker.core.templateresolver.TemplateLoader; import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormat; import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateNameFormatFM2; -import org.apache.freemarker.core.util.FTLUtil; -import org.apache.freemarker.core.util.GenericParseException; -import org.apache.freemarker.core.util._NullArgumentException; -import org.apache.freemarker.core.util._SortedArraySet; -import org.apache.freemarker.core.util._StringUtil; +import org.apache.freemarker.core.util.*; import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory; import org.apache.freemarker.core.valueformat.TemplateNumberFormat; import org.apache.freemarker.core.valueformat.TemplateNumberFormatFactory; @@ -367,7 +366,7 @@ public class Configurable { dateTimeFormat = ""; templateExceptionHandler = _TemplateAPI.getDefaultTemplateExceptionHandler( incompatibleImprovements); - arithmeticEngine = ArithmeticEngine.BIGDECIMAL_ENGINE; + arithmeticEngine = BigDecimalArithmeticEngine.INSTANCE; objectWrapper = Configuration.getDefaultObjectWrapper(incompatibleImprovements); autoFlush = Boolean.TRUE; newBuiltinClassResolver = TemplateClassResolver.UNRESTRICTED_RESOLVER; @@ -1145,7 +1144,7 @@ public class Configurable { /** * Sets the arithmetic engine used to perform arithmetic operations. - * The default is {@link ArithmeticEngine#BIGDECIMAL_ENGINE}. + * The default is {@link BigDecimalArithmeticEngine#INSTANCE}. */ public void setArithmeticEngine(ArithmeticEngine arithmeticEngine) { _NullArgumentException.check("arithmeticEngine", arithmeticEngine); @@ -2193,9 +2192,9 @@ public class Configurable { } else if (ARITHMETIC_ENGINE_KEY_SNAKE_CASE.equals(name) || ARITHMETIC_ENGINE_KEY_CAMEL_CASE.equals(name)) { if (value.indexOf('.') == -1) { if ("bigdecimal".equalsIgnoreCase(value)) { - setArithmeticEngine(ArithmeticEngine.BIGDECIMAL_ENGINE); + setArithmeticEngine(BigDecimalArithmeticEngine.INSTANCE); } else if ("conservative".equalsIgnoreCase(value)) { - setArithmeticEngine(ArithmeticEngine.CONSERVATIVE_ENGINE); + setArithmeticEngine(ConservativeArithmeticEngine.INSTANCE); } else { throw invalidSettingValueException(name, value); } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/ParserConfiguration.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/ParserConfiguration.java b/src/main/java/org/apache/freemarker/core/ParserConfiguration.java index a72ba36..0ed1424 100644 --- a/src/main/java/org/apache/freemarker/core/ParserConfiguration.java +++ b/src/main/java/org/apache/freemarker/core/ParserConfiguration.java @@ -18,6 +18,7 @@ */ package org.apache.freemarker.core; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; import org.apache.freemarker.core.outputformat.OutputFormat; /** http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java b/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java index aaea96e..7b34a70 100644 --- a/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java +++ b/src/main/java/org/apache/freemarker/core/TemplateConfiguration.java @@ -26,6 +26,7 @@ import java.util.Locale; import java.util.Map; import java.util.TimeZone; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; import org.apache.freemarker.core.model.ObjectWrapper; import org.apache.freemarker.core.outputformat.OutputFormat; import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateResolver; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/_EvalUtil.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/_EvalUtil.java b/src/main/java/org/apache/freemarker/core/_EvalUtil.java index fb0e7a5..f068774 100644 --- a/src/main/java/org/apache/freemarker/core/_EvalUtil.java +++ b/src/main/java/org/apache/freemarker/core/_EvalUtil.java @@ -21,6 +21,8 @@ package org.apache.freemarker.core; import java.util.Date; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.arithmetic.impl.BigDecimalArithmeticEngine; import org.apache.freemarker.core.model.TemplateBooleanModel; import org.apache.freemarker.core.model.TemplateCollectionModel; import org.apache.freemarker.core.model.TemplateDateModel; @@ -219,7 +221,7 @@ public class _EvalUtil { ? env.getArithmeticEngine() : (leftExp != null ? leftExp.getTemplate().getArithmeticEngine() - : ArithmeticEngine.BIGDECIMAL_ENGINE); + : BigDecimalArithmeticEngine.INSTANCE); try { cmpResult = ae.compareNumbers(leftNum, rightNum); } catch (RuntimeException e) { http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/_ParserConfigurationWithInheritedFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/_ParserConfigurationWithInheritedFormat.java b/src/main/java/org/apache/freemarker/core/_ParserConfigurationWithInheritedFormat.java index 1360d43..cf239fa 100644 --- a/src/main/java/org/apache/freemarker/core/_ParserConfigurationWithInheritedFormat.java +++ b/src/main/java/org/apache/freemarker/core/_ParserConfigurationWithInheritedFormat.java @@ -18,6 +18,7 @@ */ package org.apache.freemarker.core; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; import org.apache.freemarker.core.outputformat.OutputFormat; /** http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.java b/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.java new file mode 100644 index 0000000..43fc3c3 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/arithmetic/ArithmeticEngine.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.freemarker.core.arithmetic; + +import java.math.BigDecimal; + +import org.apache.freemarker.core.Configuration; +import org.apache.freemarker.core.TemplateException; +import org.apache.freemarker.core.util._StringUtil; + +/** + * Implements the arithmetic operations executed by the template language; see + * {@link Configuration#setArithmeticEngine(ArithmeticEngine)}. + */ +public abstract class ArithmeticEngine { + + public abstract int compareNumbers(Number first, Number second) throws TemplateException; + public abstract Number add(Number first, Number second) throws TemplateException; + public abstract Number subtract(Number first, Number second) throws TemplateException; + public abstract Number multiply(Number first, Number second) throws TemplateException; + public abstract Number divide(Number first, Number second) throws TemplateException; + public abstract Number modulus(Number first, Number second) throws TemplateException; + // [FM3] Add negate (should keep the Number type even for BigDecimalArithmeticEngine, unlike multiply). Then fix + // the negate operation in the template language. + + /** + * Should be able to parse all FTL numerical literals, Java Double toString results, and XML Schema numbers. + * This means these should be parsed successfully, except if the arithmetical engine + * couldn't support the resulting value anyway (such as NaN, infinite, even non-integers): + * {@code -123.45}, {@code 1.5e3}, {@code 1.5E3}, {@code 0005}, {@code +0}, {@code -0}, {@code NaN}, + * {@code INF}, {@code -INF}, {@code Infinity}, {@code -Infinity}. + */ + public abstract Number toNumber(String s); + + protected int minScale = 12; + protected int maxScale = 12; + protected int roundingPolicy = BigDecimal.ROUND_HALF_UP; + + /** + * Sets the minimal scale to use when dividing BigDecimal numbers. Default + * value is 12. + */ + public void setMinScale(int minScale) { + if (minScale < 0) { + throw new IllegalArgumentException("minScale < 0"); + } + this.minScale = minScale; + } + + /** + * Sets the maximal scale to use when multiplying BigDecimal numbers. + * Default value is 100. + */ + public void setMaxScale(int maxScale) { + if (maxScale < minScale) { + throw new IllegalArgumentException("maxScale < minScale"); + } + this.maxScale = maxScale; + } + + public void setRoundingPolicy(int roundingPolicy) { + if (roundingPolicy != BigDecimal.ROUND_CEILING + && roundingPolicy != BigDecimal.ROUND_DOWN + && roundingPolicy != BigDecimal.ROUND_FLOOR + && roundingPolicy != BigDecimal.ROUND_HALF_DOWN + && roundingPolicy != BigDecimal.ROUND_HALF_EVEN + && roundingPolicy != BigDecimal.ROUND_HALF_UP + && roundingPolicy != BigDecimal.ROUND_UNNECESSARY + && roundingPolicy != BigDecimal.ROUND_UP) { + throw new IllegalArgumentException("invalid rounding policy"); + } + + this.roundingPolicy = roundingPolicy; + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java b/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java new file mode 100644 index 0000000..ade8ff7 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/arithmetic/impl/BigDecimalArithmeticEngine.java @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.freemarker.core.arithmetic.impl; + +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.util._NumberUtil; + +import java.math.BigDecimal; + +/** + * Arithmetic engine that converts all numbers to {@link BigDecimal} and + * then operates on them. This is FreeMarker's default arithmetic engine. + */ +public class BigDecimalArithmeticEngine extends ArithmeticEngine { + + public static final BigDecimalArithmeticEngine INSTANCE = new BigDecimalArithmeticEngine(); + + protected BigDecimalArithmeticEngine() { + // + } + + @Override + public int compareNumbers(Number first, Number second) { + // We try to find the result based on the sign (+/-/0) first, because: + // - It's much faster than converting to BigDecial, and comparing to 0 is the most common comparison. + // - It doesn't require any type conversions, and thus things like "Infinity > 0" won't fail. + int firstSignum = _NumberUtil.getSignum(first); + int secondSignum = _NumberUtil.getSignum(second); + if (firstSignum != secondSignum) { + return firstSignum < secondSignum ? -1 : (firstSignum > secondSignum ? 1 : 0); + } else if (firstSignum == 0 && secondSignum == 0) { + return 0; + } else { + BigDecimal left = _NumberUtil.toBigDecimal(first); + BigDecimal right = _NumberUtil.toBigDecimal(second); + return left.compareTo(right); + } + } + + @Override + public Number add(Number first, Number second) { + BigDecimal left = _NumberUtil.toBigDecimal(first); + BigDecimal right = _NumberUtil.toBigDecimal(second); + return left.add(right); + } + + @Override + public Number subtract(Number first, Number second) { + BigDecimal left = _NumberUtil.toBigDecimal(first); + BigDecimal right = _NumberUtil.toBigDecimal(second); + return left.subtract(right); + } + + @Override + public Number multiply(Number first, Number second) { + BigDecimal left = _NumberUtil.toBigDecimal(first); + BigDecimal right = _NumberUtil.toBigDecimal(second); + BigDecimal result = left.multiply(right); + if (result.scale() > maxScale) { + result = result.setScale(maxScale, roundingPolicy); + } + return result; + } + + @Override + public Number divide(Number first, Number second) { + BigDecimal left = _NumberUtil.toBigDecimal(first); + BigDecimal right = _NumberUtil.toBigDecimal(second); + return divide(left, right); + } + + @Override + public Number modulus(Number first, Number second) { + long left = first.longValue(); + long right = second.longValue(); + return Long.valueOf(left % right); + } + + @Override + public Number toNumber(String s) { + return _NumberUtil.toBigDecimalOrDouble(s); + } + + private BigDecimal divide(BigDecimal left, BigDecimal right) { + int scale1 = left.scale(); + int scale2 = right.scale(); + int scale = Math.max(scale1, scale2); + scale = Math.max(minScale, scale); + return left.divide(right, scale, roundingPolicy); + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java b/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java new file mode 100644 index 0000000..45f9509 --- /dev/null +++ b/src/main/java/org/apache/freemarker/core/arithmetic/impl/ConservativeArithmeticEngine.java @@ -0,0 +1,381 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.freemarker.core.arithmetic.impl; + +import org.apache.freemarker.core.TemplateException; +import org.apache.freemarker.core._MiscTemplateException; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.util.BugException; +import org.apache.freemarker.core.util._NumberUtil; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.Map; + +/** + * Arithmetic engine that uses (more-or-less) the widening conversions of + * Java language to determine the type of result of operation, instead of + * converting everything to BigDecimal up front. + * <p> + * Widening conversions occur in following situations: + * <ul> + * <li>byte and short are always widened to int (alike to Java language).</li> + * <li>To preserve magnitude: when operands are of different types, the + * result type is the type of the wider operand.</li> + * <li>to avoid overflows: if add, subtract, or multiply would overflow on + * integer types, the result is widened from int to long, or from long to + * BigInteger.</li> + * <li>to preserve fractional part: if a division of integer types would + * have a fractional part, int and long are converted to double, and + * BigInteger is converted to BigDecimal. An operation on a float and a + * long results in a double. An operation on a float or double and a + * BigInteger results in a BigDecimal.</li> + * </ul> + */ +// [FM3] Review +public class ConservativeArithmeticEngine extends ArithmeticEngine { + + public static final ConservativeArithmeticEngine INSTANCE = new ConservativeArithmeticEngine(); + + private static final int INTEGER = 0; + private static final int LONG = 1; + private static final int FLOAT = 2; + private static final int DOUBLE = 3; + private static final int BIG_INTEGER = 4; + private static final int BIG_DECIMAL = 5; + + private static final Map classCodes = createClassCodesMap(); + + protected ConservativeArithmeticEngine() { + // + } + + @Override + public int compareNumbers(Number first, Number second) throws TemplateException { + switch (getCommonClassCode(first, second)) { + case INTEGER: { + int n1 = first.intValue(); + int n2 = second.intValue(); + return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); + } + case LONG: { + long n1 = first.longValue(); + long n2 = second.longValue(); + return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); + } + case FLOAT: { + float n1 = first.floatValue(); + float n2 = second.floatValue(); + return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); + } + case DOUBLE: { + double n1 = first.doubleValue(); + double n2 = second.doubleValue(); + return n1 < n2 ? -1 : (n1 == n2 ? 0 : 1); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + return n1.compareTo(n2); + } + case BIG_DECIMAL: { + BigDecimal n1 = _NumberUtil.toBigDecimal(first); + BigDecimal n2 = _NumberUtil.toBigDecimal(second); + return n1.compareTo(n2); + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new Error(); + } + + @Override + public Number add(Number first, Number second) throws TemplateException { + switch(getCommonClassCode(first, second)) { + case INTEGER: { + int n1 = first.intValue(); + int n2 = second.intValue(); + int n = n1 + n2; + return + ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check + ? Long.valueOf(((long) n1) + n2) + : Integer.valueOf(n); + } + case LONG: { + long n1 = first.longValue(); + long n2 = second.longValue(); + long n = n1 + n2; + return + ((n ^ n1) < 0 && (n ^ n2) < 0) // overflow check + ? toBigInteger(first).add(toBigInteger(second)) + : Long.valueOf(n); + } + case FLOAT: { + return Float.valueOf(first.floatValue() + second.floatValue()); + } + case DOUBLE: { + return Double.valueOf(first.doubleValue() + second.doubleValue()); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + return n1.add(n2); + } + case BIG_DECIMAL: { + BigDecimal n1 = _NumberUtil.toBigDecimal(first); + BigDecimal n2 = _NumberUtil.toBigDecimal(second); + return n1.add(n2); + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new Error(); + } + + @Override + public Number subtract(Number first, Number second) throws TemplateException { + switch(getCommonClassCode(first, second)) { + case INTEGER: { + int n1 = first.intValue(); + int n2 = second.intValue(); + int n = n1 - n2; + return + ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check + ? Long.valueOf(((long) n1) - n2) + : Integer.valueOf(n); + } + case LONG: { + long n1 = first.longValue(); + long n2 = second.longValue(); + long n = n1 - n2; + return + ((n ^ n1) < 0 && (n ^ ~n2) < 0) // overflow check + ? toBigInteger(first).subtract(toBigInteger(second)) + : Long.valueOf(n); + } + case FLOAT: { + return Float.valueOf(first.floatValue() - second.floatValue()); + } + case DOUBLE: { + return Double.valueOf(first.doubleValue() - second.doubleValue()); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + return n1.subtract(n2); + } + case BIG_DECIMAL: { + BigDecimal n1 = _NumberUtil.toBigDecimal(first); + BigDecimal n2 = _NumberUtil.toBigDecimal(second); + return n1.subtract(n2); + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new Error(); + } + + @Override + public Number multiply(Number first, Number second) throws TemplateException { + switch(getCommonClassCode(first, second)) { + case INTEGER: { + int n1 = first.intValue(); + int n2 = second.intValue(); + int n = n1 * n2; + return + n1 == 0 || n / n1 == n2 // overflow check + ? Integer.valueOf(n) + : Long.valueOf(((long) n1) * n2); + } + case LONG: { + long n1 = first.longValue(); + long n2 = second.longValue(); + long n = n1 * n2; + return + n1 == 0L || n / n1 == n2 // overflow check + ? Long.valueOf(n) + : toBigInteger(first).multiply(toBigInteger(second)); + } + case FLOAT: { + return Float.valueOf(first.floatValue() * second.floatValue()); + } + case DOUBLE: { + return Double.valueOf(first.doubleValue() * second.doubleValue()); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + return n1.multiply(n2); + } + case BIG_DECIMAL: { + BigDecimal n1 = _NumberUtil.toBigDecimal(first); + BigDecimal n2 = _NumberUtil.toBigDecimal(second); + BigDecimal r = n1.multiply(n2); + return r.scale() > maxScale ? r.setScale(maxScale, roundingPolicy) : r; + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new Error(); + } + + @Override + public Number divide(Number first, Number second) throws TemplateException { + switch(getCommonClassCode(first, second)) { + case INTEGER: { + int n1 = first.intValue(); + int n2 = second.intValue(); + if (n1 % n2 == 0) { + return Integer.valueOf(n1 / n2); + } + return Double.valueOf(((double) n1) / n2); + } + case LONG: { + long n1 = first.longValue(); + long n2 = second.longValue(); + if (n1 % n2 == 0) { + return Long.valueOf(n1 / n2); + } + return Double.valueOf(((double) n1) / n2); + } + case FLOAT: { + return Float.valueOf(first.floatValue() / second.floatValue()); + } + case DOUBLE: { + return Double.valueOf(first.doubleValue() / second.doubleValue()); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + BigInteger[] divmod = n1.divideAndRemainder(n2); + if (divmod[1].equals(BigInteger.ZERO)) { + return divmod[0]; + } else { + BigDecimal bd1 = new BigDecimal(n1); + BigDecimal bd2 = new BigDecimal(n2); + return bd1.divide(bd2, minScale, roundingPolicy); + } + } + case BIG_DECIMAL: { + BigDecimal n1 = _NumberUtil.toBigDecimal(first); + BigDecimal n2 = _NumberUtil.toBigDecimal(second); + int scale1 = n1.scale(); + int scale2 = n2.scale(); + int scale = Math.max(scale1, scale2); + scale = Math.max(minScale, scale); + return n1.divide(n2, scale, roundingPolicy); + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new Error(); + } + + @Override + public Number modulus(Number first, Number second) throws TemplateException { + switch(getCommonClassCode(first, second)) { + case INTEGER: { + return Integer.valueOf(first.intValue() % second.intValue()); + } + case LONG: { + return Long.valueOf(first.longValue() % second.longValue()); + } + case FLOAT: { + return Float.valueOf(first.floatValue() % second.floatValue()); + } + case DOUBLE: { + return Double.valueOf(first.doubleValue() % second.doubleValue()); + } + case BIG_INTEGER: { + BigInteger n1 = toBigInteger(first); + BigInteger n2 = toBigInteger(second); + return n1.mod(n2); + } + case BIG_DECIMAL: { + throw new _MiscTemplateException("Can't calculate remainder on BigDecimals"); + } + } + // Make the compiler happy. getCommonClassCode() is guaranteed to + // return only above codes, or throw an exception. + throw new BugException(); + } + + @Override + public Number toNumber(String s) { + Number n = _NumberUtil.toBigDecimalOrDouble(s); + return n instanceof BigDecimal ? _NumberUtil.optimizeNumberRepresentation(n) : n; + } + + private static Map createClassCodesMap() { + Map map = new HashMap(17); + Integer intcode = Integer.valueOf(INTEGER); + map.put(Byte.class, intcode); + map.put(Short.class, intcode); + map.put(Integer.class, intcode); + map.put(Long.class, Integer.valueOf(LONG)); + map.put(Float.class, Integer.valueOf(FLOAT)); + map.put(Double.class, Integer.valueOf(DOUBLE)); + map.put(BigInteger.class, Integer.valueOf(BIG_INTEGER)); + map.put(BigDecimal.class, Integer.valueOf(BIG_DECIMAL)); + return map; + } + + private static int getClassCode(Number num) throws TemplateException { + try { + return ((Integer) classCodes.get(num.getClass())).intValue(); + } catch (NullPointerException e) { + if (num == null) { + throw new _MiscTemplateException("The Number object was null."); + } else { + throw new _MiscTemplateException("Unknown number type ", num.getClass().getName()); + } + } + } + + private static int getCommonClassCode(Number num1, Number num2) throws TemplateException { + int c1 = getClassCode(num1); + int c2 = getClassCode(num2); + int c = c1 > c2 ? c1 : c2; + // If BigInteger is combined with a Float or Double, the result is a + // BigDecimal instead of BigInteger in order not to lose the + // fractional parts. If Float is combined with Long, the result is a + // Double instead of Float to preserve the bigger bit width. + switch (c) { + case FLOAT: { + if ((c1 < c2 ? c1 : c2) == LONG) { + return DOUBLE; + } + break; + } + case BIG_INTEGER: { + int min = c1 < c2 ? c1 : c2; + if (min == DOUBLE || min == FLOAT) { + return BIG_DECIMAL; + } + break; + } + } + return c; + } + + private static BigInteger toBigInteger(Number num) { + return num instanceof BigInteger ? (BigInteger) num : new BigInteger(num.toString()); + } +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/model/TemplateNumberModel.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/model/TemplateNumberModel.java b/src/main/java/org/apache/freemarker/core/model/TemplateNumberModel.java index 3d1695c..6ca340a 100644 --- a/src/main/java/org/apache/freemarker/core/model/TemplateNumberModel.java +++ b/src/main/java/org/apache/freemarker/core/model/TemplateNumberModel.java @@ -19,7 +19,7 @@ package org.apache.freemarker.core.model; -import org.apache.freemarker.core.ArithmeticEngine; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; /** * "number" template language data type; an object that stores a number. There's only one numerical type as far as the http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java b/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java index 0e9825a..1b743f7 100644 --- a/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java +++ b/src/main/java/org/apache/freemarker/core/util/_NumberUtil.java @@ -202,4 +202,27 @@ public class _NumberUtil { } return number; } + + public static BigDecimal toBigDecimal(Number num) { + try { + return num instanceof BigDecimal ? (BigDecimal) num : new BigDecimal(num.toString()); + } catch (NumberFormatException e) { + // The exception message is useless, so we add a new one: + throw new NumberFormatException("Can't parse this as BigDecimal number: " + _StringUtil.jQuote(num)); + } + } + + public static Number toBigDecimalOrDouble(String s) { + if (s.length() > 2) { + char c = s.charAt(0); + if (c == 'I' && (s.equals("INF") || s.equals("Infinity"))) { + return Double.valueOf(Double.POSITIVE_INFINITY); + } else if (c == 'N' && s.equals("NaN")) { + return Double.valueOf(Double.NaN); + } else if (c == '-' && s.charAt(1) == 'I' && (s.equals("-INF") || s.equals("-Infinity"))) { + return Double.valueOf(Double.NEGATIVE_INFINITY); + } + } + return new BigDecimal(s); + } } http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java ---------------------------------------------------------------------- diff --git a/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java b/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java index 44068e4..de454c9 100644 --- a/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java +++ b/src/main/java/org/apache/freemarker/core/valueformat/TemplateNumberFormat.java @@ -20,7 +20,7 @@ package org.apache.freemarker.core.valueformat; import java.text.NumberFormat; -import org.apache.freemarker.core.ArithmeticEngine; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; import org.apache.freemarker.core.Environment; import org.apache.freemarker.core.model.TemplateDateModel; import org.apache.freemarker.core.model.TemplateMarkupOutputModel; http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/manual/en_US/FM3-CHANGE-LOG.txt ---------------------------------------------------------------------- diff --git a/src/manual/en_US/FM3-CHANGE-LOG.txt b/src/manual/en_US/FM3-CHANGE-LOG.txt index 94b62ad..2f2ac35 100644 --- a/src/manual/en_US/FM3-CHANGE-LOG.txt +++ b/src/manual/en_US/FM3-CHANGE-LOG.txt @@ -72,6 +72,7 @@ the FreeMarer 3 changelog here: TemplateResolver, which is the central class of loading and caching and template name rules). OutputFormat realted classes were moved to org.apache.freemarker.core.outputformat. ValueFormat related classes were moved to org.apache.freemarker.core.valueformat. + ArithmeticEngine related classes were moved to org.apache.freemarker.core.arithmetic. freemarker.ext.beans were moved under org.apache.freemarker.core.model.impl.beans for now (but later we only want a DefaultObject wrapper, no BeansWrapper, so this will change) and freemarker.ext.dom was moved to org.apache.freemarker.core.model.impl.dom. http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/test/java/org/apache/freemarker/core/ObjectBuilderSettingsTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/ObjectBuilderSettingsTest.java b/src/test/java/org/apache/freemarker/core/ObjectBuilderSettingsTest.java index 67336e0..7b21b54 100644 --- a/src/test/java/org/apache/freemarker/core/ObjectBuilderSettingsTest.java +++ b/src/test/java/org/apache/freemarker/core/ObjectBuilderSettingsTest.java @@ -46,6 +46,8 @@ import java.util.Map; import java.util.Properties; import java.util.TimeZone; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.arithmetic.impl.BigDecimalArithmeticEngine; import org.apache.freemarker.core.model.ObjectWrapper; import org.apache.freemarker.core.model.impl.DefaultObjectWrapper; import org.apache.freemarker.core.model.impl.beans.BeansWrapper; @@ -447,7 +449,7 @@ public class ObjectBuilderSettingsTest { props.setProperty(Configurable.TEMPLATE_EXCEPTION_HANDLER_KEY, "rethrow"); cfg.setSettings(props); assertEquals(BeansWrapper.class, cfg.getObjectWrapper().getClass()); - assertSame(ArithmeticEngine.BIGDECIMAL_ENGINE, cfg.getArithmeticEngine()); + assertSame(BigDecimalArithmeticEngine.INSTANCE, cfg.getArithmeticEngine()); assertSame(TemplateExceptionHandler.RETHROW_HANDLER, cfg.getTemplateExceptionHandler()); assertTrue(((WriteProtectable) cfg.getObjectWrapper()).isWriteProtected()); assertEquals(Configuration.VERSION_3_0_0, http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/dcb0e063/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java ---------------------------------------------------------------------- diff --git a/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java b/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java index 1348ac0..825e92d 100644 --- a/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java +++ b/src/test/java/org/apache/freemarker/core/TemplateConfigurationTest.java @@ -49,6 +49,8 @@ import java.util.Set; import java.util.TimeZone; import org.apache.commons.collections.ListUtils; +import org.apache.freemarker.core.arithmetic.ArithmeticEngine; +import org.apache.freemarker.core.arithmetic.impl.ConservativeArithmeticEngine; import org.apache.freemarker.core.model.impl.SimpleObjectWrapper; import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat; import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat; @@ -183,7 +185,7 @@ public class TemplateConfigurationTest { SETTING_ASSIGNMENTS.put("templateExceptionHandler", TemplateExceptionHandler.IGNORE_HANDLER); SETTING_ASSIGNMENTS.put("timeFormat", "@HH:mm"); SETTING_ASSIGNMENTS.put("timeZone", NON_DEFAULT_TZ); - SETTING_ASSIGNMENTS.put("arithmeticEngine", ArithmeticEngine.CONSERVATIVE_ENGINE); + SETTING_ASSIGNMENTS.put("arithmeticEngine", ConservativeArithmeticEngine.INSTANCE); SETTING_ASSIGNMENTS.put("customNumberFormats", ImmutableMap.of("dummy", HexTemplateNumberFormatFactory.INSTANCE)); SETTING_ASSIGNMENTS.put("customDateFormats",