<NME> FormulaTest.java
<BEF> package cgeo.geocaching.utils.formulas;

import cgeo.geocaching.utils.KeyableCharSet;
import cgeo.geocaching.utils.TextUtils;
import cgeo.geocaching.utils.functions.Func1;
import static cgeo.geocaching.utils.formulas.FormulaException.ErrorType.MISSING_VARIABLE_VALUE;
import static cgeo.geocaching.utils.formulas.FormulaException.ErrorType.OTHER;
import static cgeo.geocaching.utils.formulas.FormulaException.ErrorType.UNEXPECTED_TOKEN;
import static cgeo.geocaching.utils.formulas.FormulaException.ErrorType.WRONG_PARAMETER_COUNT;
import static cgeo.geocaching.utils.formulas.FormulaException.ErrorType.WRONG_TYPE;

import android.graphics.Color;
import android.text.style.ForegroundColorSpan;
import android.util.Pair;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.junit.Test;
import static org.assertj.core.api.Java6Assertions.assertThat;
import static org.assertj.core.api.Java6Assertions.assertThatThrownBy;

public class FormulaTest {


    private static double eval(final String expression, final Object... vars) {
        return Formula.eval(expression, vars);
    }

    private static Set<String> neededVars(final String expression) {
        return Formula.compile(expression).getNeededVariables();
    }

    @Test
    public void numbers() {
        assertThat(Formula.evaluate("-2.5")).isEqualTo(Value.of(-2.5d));
        assertThat(Formula.evaluate("2,5")).isEqualTo(Value.of(2.5d));
        assertThat(Formula.evaluate("2,5.5")).isEqualTo(Value.of("2,5.5"));
    }

    @Test
    public void bigIntegers() {
        //Long value
        final String longMax = String.valueOf(Long.MAX_VALUE); // this should be 9223372036854775807
        final int longMaxCs = 88; // manually calculated checksum of Long.MAX_VALUE

        assertThat(Formula.evaluate(longMax).isInteger()).isTrue();
        assertThat(Formula.evaluate(longMax).getAsInt()).isEqualTo(Long.MAX_VALUE);
        //Beyond Long Value
        final String longMaxBeyond = longMax + "1";
        final Value longMaxBeyondValue = Formula.evaluate(longMaxBeyond);
        assertThat(longMaxBeyondValue.isInteger()).isFalse();
        assertThat(longMaxBeyondValue.getAsInt()).isEqualTo(0L); // if too long for parsing -> return 0

        //Long value checksums
    }

    @Test
    public void concatWithParenthesis() {
        assertThat(eval("(3(5))")).isEqualTo(35d);
        assertThat(eval("(3(5)AA)", "A", 1)).isEqualTo(3511d);
        assertThat(eval("3(5)A(A+1)", "A", 1)).isEqualTo(3512d);
    }

    @Test
    public void variableCalculations() {
        assertThat(eval("AA / 2 + B", "A", 4, "B", 5)).isEqualTo(27d);

        //whitespaces
        assertThat(eval("3 4")).isEqualTo(34d);
        assertThat(eval("3 A   4.3", "A", 12)).isEqualTo(3124.3d);
        assertThat(eval("A+1 4", "A", 12)).isEqualTo(134d);
        assertThat(eval("3 A+1 4.3", "A", 12)).isEqualTo(3134.3d);
        assertThat(eval("3 A+1+4.3", "A", 12)).isEqualTo(317.3d);
        assertThat(eval("3 A+1 4.3", "A", 1.2)).isEqualTo(0d);

        assertThat(eval("3 A + 1 4.3", "A", 2)).isEqualTo(334.3d);
        assertThat(eval("3 (A+1 A + 2) 4.3", "A", 2)).isEqualTo(3344.3d);

        //issue #12610
        assertThat(eval("2+ 3 4 +5")).isEqualTo(59d);
        assertThat(eval("2+ 3 4+5")).isEqualTo(59d);
        assertThat(eval("1 + 2 3")).isEqualTo(33d);
        assertThat(eval("2+(3 4)+5")).isEqualTo(41d);
    }


    @Test
    public void variableCalculations() {
        assertThat(eval("AA / 2 + B", "A", 4, "B", 5)).isEqualTo(27d);
        assertThat(eval("sqrt(A*B) * C", "A", 4, "B", 4, "C", 10)).isEqualTo(40d);
        assertThat(eval("AB + A + B", "AB", 14, "B", 4, "A", 3)).isEqualTo(41d);
        assertThat(eval("$A$B + A + B", "AB", 14, "B", 4, "A", 3)).isEqualTo(41d);
        assertThat(eval("$AB + A + $B", "AB", 14, "B", 4, "A", 3)).isEqualTo(21d);
        assertThat(eval("$AB(A+$B*2)$B", "AB", 14, "B", 4, "A", 3)).isEqualTo(14114d);

        assertThat(eval("A$A$AB", "A", 1, "AB", 2)).isEqualTo(112);
        assertThat(eval("-A$A$AB", "A", 1, "AB", 2)).isEqualTo(-112);
        assertThat(eval("-A$A$AB", "A", 0, "AB", 2)).isEqualTo(-2);
        assertThatThrownBy(() -> eval("AA$", "A", 1))
                .isInstanceOf(FormulaException.class).hasMessageContaining(UNEXPECTED_TOKEN.name());

        assertThat(eval("-A", "A", 1)).isEqualTo(-1);
        assertThat(eval("A", "A", -1)).isEqualTo(-1);
        assertThat(Formula.evaluate("AA", "A", -1).getAsString()).isEqualTo("-1-1");
    }

    @Test
    public void variablesWithDecimals() {

        assertThat(eval("3.14A5", "A", 1)).isEqualTo(3.1415d);
        assertThat(eval("3.A4A5", "A", 1)).isEqualTo(3.1415d);
        assertThat(eval("A.(A+1)(A+10)", "A", 1)).isEqualTo(1.211d);
        assertThat(eval("-A.(A+1)(A+10)", "A", 1)).isEqualTo(-1.211d);

        assertThat(eval("A15B", "A", 3.14, "B", 92)).isEqualTo(3.141592d);
        assertThat(Formula.evaluate("AA", "A", 1.2).getAsString()).isEqualTo("1.21.2");

        assertThat(eval("A.A", "A", 1)).isEqualTo(1.1d);
        assertThat(Formula.evaluate("A.A.A", "A", 1).getAsString()).isEqualTo("1.1.1");
        assertThat(Formula.evaluate("...", "A", 1).getAsString()).isEqualTo("...");
        assertThat(eval(".12")).isEqualTo(.12d);

    }

    @Test
    public void variableParsing() {
        assertThat(Formula.compile("A").getNeededVariables()).containsExactlyInAnyOrder("A");
        assertThat(Formula.compile("AB").getNeededVariables()).containsExactlyInAnyOrder("A", "B");
        assertThat(Formula.compile("AB").getNeededVariables()).containsExactlyInAnyOrder("A", "B");
        assertThat(Formula.compile("ABa").getNeededVariables()).containsExactlyInAnyOrder("A", "B", "a");
        assertThat(Formula.compile("AB1").getNeededVariables()).containsExactlyInAnyOrder("A", "B");
        assertThat(Formula.compile("$AB1").getNeededVariables()).containsExactlyInAnyOrder("AB1");
        assertThat(Formula.compile("$AB(1)").getNeededVariables()).containsExactlyInAnyOrder("AB");
        assertThat(Formula.compile("${AB}A1").getNeededVariables()).containsExactlyInAnyOrder("AB", "A");
        assertThat(Formula.compile("${A}A1").getNeededVariables()).containsExactlyInAnyOrder("A");

        assertThatThrownBy(() -> eval("$"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(UNEXPECTED_TOKEN.name());
        assertThatThrownBy(() -> eval("$1"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(UNEXPECTED_TOKEN.name());
        assertThatThrownBy(() -> eval("${A(B)"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(UNEXPECTED_TOKEN.name()).hasMessageContaining("}");
    }

    @Test
    public void ranges() {
        //assert handling of "normal" formats
        assertThat(Formula.evaluateWithRanges("[0-9]", 3)).isEqualTo(Value.of(3));
        assertThat(Formula.compile("[0-9]").getRangeIndexSize()).isEqualTo(10);

        assertThat(Formula.compile("[0-3]*[0-4]").getRangeIndexSize()).isEqualTo(20);
        assertRangeFormula("[1-2]*[3-4]", null, 3, 6, 4, 8);
        assertRangeFormula("[1-3, ^2]*[10-20, ^11-19]", null, 10, 30, 20, 60);

        //assert graceful handling of "strange" formats
        assertRangeFormula("[abc1]", null, 1);
        assertRangeFormula("[2-]", null, 2);
        assertRangeFormula("[-3]", null, 3);

        //assert
        assertThatThrownBy(() -> eval("[0-3"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(UNEXPECTED_TOKEN.name()).hasMessageContaining("]");
        assertThatThrownBy(() -> eval("[0-5000]")).as("Too many items in range")
                .isInstanceOf(FormulaException.class).hasMessageContaining(OTHER.name()).hasMessageContaining("0-5000");
        assertThatThrownBy(() -> eval("[]"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(OTHER.name());
        assertThatThrownBy(() -> eval("[  ]"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(OTHER.name());
        assertThatThrownBy(() -> eval("[^1-2]"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(OTHER.name());

    }

    private void assertRangeFormula(final String formula, final Func1<String, Value> varMap, final Object... expectedResults) {
        final Formula f = Formula.compile(formula);
        assertThat(f.getRangeIndexSize()).isEqualTo(expectedResults.length);
        final List<Value> results = new ArrayList<>();
        for (int i = 0; i < f.getRangeIndexSize(); i++) {
            results.add(f.evaluate(varMap, i));
        }
        final List<Value> expectedResultList = new ArrayList<>();
        for (Object o : expectedResults) {
            expectedResultList.add(o instanceof Value ? (Value) o : Value.of(o));
        }
        assertThat(results).containsExactlyElementsOf(expectedResultList);
    }

    @Test
    public void strings() {
        assertThat(Formula.evaluate("'abc'")).isEqualTo(Value.of("abc"));
        assertThat(Formula.evaluate("'a\"b''c'")).isEqualTo(Value.of("a\"b'c"));
        assertThat(Formula.evaluate("\"abc\"")).isEqualTo(Value.of("abc"));
        assertThat(Formula.evaluate("\"a\"\"b'c\"")).isEqualTo(Value.of("a\"b'c"));
        assertThatThrownBy(() -> eval("\"abc'"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(UNEXPECTED_TOKEN.name())
                .hasMessageContaining("\"");

        assertThat(eval("length('abc')")).isEqualTo(3d);
        assertThat(eval("length('abc') + length('def')")).isEqualTo(6d);
        assertThat(eval("length('abc''def')")).isEqualTo(7d);
        assertThat(eval("length(A)", "A", "test")).isEqualTo(4d);
        assertThat(eval("length(AA)", "A", "test")).isEqualTo(8d);
        assertThat(eval("length(A'def'123)", "A", "test")).isEqualTo(10d);
    }

    @Test
    public void stringsAndStartEndTokens() {
        assertThat(Formula.evaluate("' '").getRaw()).isEqualTo(" ");
        assertThat(eval("10- -+ +- -+ +-2")).isEqualTo(8);
    }

    @Test
    public void unknownOperator() {
        assertThatThrownBy(() -> eval("2 & 5"))
        assertThat(Formula.evaluate("rot13('abc')").getRaw()).isEqualTo("nop");
        assertThat(Formula.evaluate("rot13(rot('aBc'; -13))").getRaw()).isEqualTo("aBc");
        assertThat(Formula.evaluate("rot('abc'; 1)").getRaw()).isEqualTo("bcd");
        assertThat(eval("rot1(a)", "r", 1d, "o", 2d, "t", 3d, "a", 4d)).isEqualTo(12314d);
    }

    @Test
    public void unaryOperators() {
        assertThat(eval("(1+2)!")).isEqualTo(6);
        assertThat(eval("10--2")).isEqualTo(12);
        assertThat(eval("10- -+ +- -+ +-2")).isEqualTo(8);
    }

    @Test
    public void operatorPrecedenceAndEvaluationOrder() {
        assertThat(eval("100/5/5")).isEqualTo(4);
        assertThat(eval("100-5-5")).isEqualTo(90);
        assertThat(eval("100-5*5")).isEqualTo(75);
    }

    @Test
    public void unknownOperator() {
        assertThatThrownBy(() -> eval("2 & 5"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(UNEXPECTED_TOKEN.name());
    }

    @Test
    public void unknownVariables() {
        assertThatThrownBy(() -> eval("unknown(42)"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(MISSING_VARIABLE_VALUE.name()).hasMessageContaining("k, n, o, u, w");
    }

    @Test
    public void unclosedParentheses() {
        assertThatThrownBy(() -> eval("3 * (2 + 4 * 2"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(UNEXPECTED_TOKEN.name());
    }

    @Test
    public void parseFormulaParts() {
        assertFormulaPart("(1+2)", 0, null, "3", "(1+2)");
        assertFormulaPart("a1+2|", 1, "|", "3", "1+2");
        assertFormulaPart("1 + (10 - 2) + 5 -", 0, "-", "14", "1 + (10 - 2) + 5 ");
        assertFormulaPart("1+(10 - 2) + 5 -", 0, "- ", "9", "1+(10 - 2)");
        assertFormulaPart("111 + (10 - '2') + 5 ''", 2, "'", "14", "1 + (10 - '2') + 5 ");
    }

    private void assertFormulaPart(final String formulaString, final int startPos, final String stopChars, final String expectedValue, final String expectedFormPart) {
        final Formula f = Formula.compile(formulaString, startPos, KeyableCharSet.createFor(stopChars));
        assertThat(f.evaluate().getAsString()).isEqualTo(expectedValue);
        assertThat(f.getExpression()).isEqualTo(expectedFormPart);
    }

    @Test
    public void incorrectFunctionCall() {
        assertThatThrownBy(() -> eval("sin('abc')"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(WRONG_TYPE.name()).hasMessageContaining("sin").hasMessageContaining("abc");
        assertThatThrownBy(() -> eval("sin()"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(WRONG_PARAMETER_COUNT.name()).hasMessageContaining("sin");
    }

    @Test
    public void advancedFunctions() {
        assertThat(eval("checksum(888)")).isEqualTo(24);
        assertThat(eval("ichecksum(888)")).isEqualTo(6);
        assertThat(eval("ichecksum(-888)")).isEqualTo(6);
        assertThat(eval("ichecksum(-888.234)")).isEqualTo(6);
        assertThat(eval("lettervalue('Test123')")).isEqualTo(20 + 5 + 19 + 20 + 1 + 2 + 3);
        assertThat(eval("lettervalue(-888.123)")).isEqualTo(30);
    }

    @Test
    public void neededVariables() {
        assertThat(neededVars("abcdef")).containsExactlyInAnyOrder("a", "b", "c", "d", "e", "f");
        assertThat(neededVars("abcdef")).containsExactlyInAnyOrder("a", "b", "c", "d", "e", "f");
        assertThat(neededVars("$abcdef")).containsExactlyInAnyOrder("abcdef");
        assertThat(neededVars("Test123one")).containsExactlyInAnyOrder("T", "e", "s", "t", "o", "n");
        assertThat(neededVars("Test123$one")).containsExactlyInAnyOrder("T", "e", "s", "t", "one");
        assertThat(neededVars("B+C")).containsExactlyInAnyOrder("B", "C");

    }

    @Test
    public void value() {
        assertThat(Value.of(345).getAsInt()).isEqualTo(345);
        assertThat(Value.of("345").getAsInt()).isEqualTo(345);
        assertThat(Value.of(345).isInteger()).isTrue();
        assertThat(Value.of("345").isInteger()).isTrue();
        assertThat(Value.of("345b").isInteger()).isFalse();
        assertThat(Value.of("abcd").isInteger()).isFalse();

        assertThat(Formula.evaluate("123.00").isInteger()).isTrue();
        assertThat(Formula.evaluate("123.00").getAsInt()).isEqualTo(123L);
    }

    @Test
    public void overflow() {
        assertThat(eval("A8.A2", "A", 10)).isEqualTo(108.102d);
        assertThat(eval("_A8._A2", "A", 10)).isEqualTo(108.102d);
        assertThat(eval("_A8._A2", "A", 8)).isEqualTo(88.082d);
        assertThat(eval("8._1_A2", "A", 8)).isEqualTo(8.01082d);
        assertThat(eval("8.A__A2", "A", 14)).isEqualTo(8.140142d);
        assertThat(eval("_A._A", "A", 143)).isEqualTo(143.143d);
        assertThat(eval("_A._A", "A", 14)).isEqualTo(14.14d);
        assertThat(eval("_A._A", "A", 1)).isEqualTo(1.01d);

        //handling of _ on more strange positions
        assertThat(eval("5._1")).isEqualTo(5.01d);
        assertThat(eval("5._12")).as("number should not spill over to _").isEqualTo(5.012d);
        assertThat(eval("5._(8+1)")).as("expression results should spill over").isEqualTo(5.09d);
        assertThat(eval("5._(8+5)")).as("expression results should spill over").isEqualTo(5.13d);
        assertThat(eval("5_.12")).as("_ before . should have no effect").isEqualTo(5.12d);
        assertThat(Formula.evaluate("_5.12").getAsString()).as("_ at start should be removed").isEqualTo("5.12");
        assertThat(Formula.evaluate("5.12_").getAsString()).as("_ at end should be removed").isEqualTo("5.12");
    }

    @Test
    public void evaluateToString() {
        assertThat(Formula.compile("A.B").evaluateToString(Formula.toVarProvider("A", 1, "B", 2))).isEqualTo("1.2");
        assertThat(Formula.compile("A(A+B)B").evaluateToString(Formula.toVarProvider("A", 1, "B", 2))).isEqualTo("132");
        assertThat(Formula.compile("A.B").evaluateToString(Formula.toVarProvider("A", 1))).isEqualTo("1.?B");
        assertThat(Formula.compile("A*5").evaluateToString(Formula.toVarProvider("A", "eddie"))).isEqualTo("'eddie' * 5");
        assertThat(Formula.compile("(A*4+(3+4))").evaluateToString(Formula.toVarProvider("A", 2)))
                .isEqualTo("15");
        assertThat(Formula.compile("(A*4+(3+4))").evaluateToString(Formula.toVarProvider("A", "e")))
                .isEqualTo("('e' * 4 + 7)");
        assertThat(Formula.compile("123(4*3)B_A(A*4+(3+4))").evaluateToString(Formula.toVarProvider("A", "eddie")))
                .isEqualTo("12312?Beddie('eddie' * 4 + 7)");
    }

    @Test
    public void evaluateToCharSequence() {
        assertErrorStringFormats(Formula.compile("3-'a'").evaluateToCharSequence(null), "3 - ['a']");
        assertErrorStringFormats(Formula.compile("3+4+'a'+5").evaluateToCharSequence(null), "7 + ['a'] + 5");
        assertErrorStringFormats(Formula.compile("3+4+'a'+AB12").evaluateToCharSequence(null), "7 + ['a'] + [?A][?B]12");
        assertErrorStringFormats(Formula.compile("(5+'a')").evaluateToCharSequence(null), "(5 + ['a'])");
    }

    /**
     * checks whether a given error string is correct and has the right places highlighted with an error format
     */
    private void assertErrorStringFormats(final CharSequence cs, final String expectedString) {
        final String annotated = TextUtils.annotateSpans(cs, s -> {
            if (s instanceof ForegroundColorSpan && ((ForegroundColorSpan) s).getForegroundColor() == Color.RED) {
                return new Pair<>("[", "]");
            }
            return new Pair<>("", "");
        });

        assertThat(annotated).isEqualTo(expectedString);
    }

    @Test
    public void efficientFormulaNodeTrees() {
        assertThat(Formula.compile("13.123").toDebugString(null, false, true))
                .as("constant value should be in one node").isEqualTo("13.123{}");
        assertThat(Formula.compile("13.123(4+5)").toDebugString(null, false, true))
                .as("constant value should be in one node").isEqualTo("13.1239{}");
        assertThat(Formula.compile("13.123 + length('abc')").toDebugString(null, false, true))
                .as("constant value should be in one node").isEqualTo("16.123{}");

        assertThat(Formula.compile("A + (length('abc') + 3)").toDebugString(null, false, true))
                .as("constant parts should be in one subnode").isEqualTo("?A + 6{?A{}6{}}");

        assertThat(Formula.compile("12A.456B8901").toDebugString(null, false, true))
                .as("decimals should be stored as efficiently as possible").isEqualTo("12?A.456?B8901{12{}?A{}.456{}?B{}8901{}}");


    }

    @Test
    public void unusualCharacters() {
        assertThat(eval("(3+5) * (4+7)")).isEqualTo(88d);
        assertThat(eval("42•2")).isEqualTo(84d);
        assertThat(eval("42÷2")).isEqualTo(21d);
        assertThat(eval("42:2")).isEqualTo(21d);
        assertThat(eval("42—2")).isEqualTo(40d);
        assertThat(eval("—2")).isEqualTo(-2d);

        assertThat(eval("2²")).isEqualTo(4d);
        assertThat(eval("-4³")).isEqualTo(-64d);
        assertThat(eval("3!²")).isEqualTo(36d);
        assertThat(eval("(3²)!")).isEqualTo(2 * 3 * 4 * 5 * 6 * 7 * 8 * 9);
    }

    @Test
    public void comparison() {
        assertThat(eval("3==3")).isEqualTo(1d);
        assertThat(eval("3<>4")).isEqualTo(1d);
        assertThat(eval("3<4")).isEqualTo(1d);
        assertThat(eval("3<=4")).isEqualTo(1d);
        assertThat(eval("3>=4")).isEqualTo(0d);
        assertThat(eval("4>=4")).isEqualTo(1d);
        assertThat(eval("(3+5) > (4+7)")).isEqualTo(0d);
        assertThat(eval("(3+5) < (4+7)")).isEqualTo(1d);

        assertThatThrownBy(() -> eval("3>>4"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(UNEXPECTED_TOKEN.name()).hasMessageContaining(">");
        assertThatThrownBy(() -> eval("3<<4"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(UNEXPECTED_TOKEN.name()).hasMessageContaining("<");
        assertThatThrownBy(() -> eval("3=>4"))
                .isInstanceOf(FormulaException.class).hasMessageContaining(UNEXPECTED_TOKEN.name()).hasMessageContaining(">");

        assertThat(eval("if(3>4;1;2)")).isEqualTo(2d);
        assertThat(eval("if(3<4;1;2)")).isEqualTo(1d);

    }

    @Test
    public void checkFormatter() {
        final char c = 'E';
        final double d = 9.1324567934d;
        assertThat(String.format(Locale.US, "%c%09.5f°", c, d)).isEqualTo("E009.13246°");
    }

    @Test
    public void userComments() {
        assertThat(eval("123#4")).isEqualTo(123d);
        assertThat(eval("123# 4")).isEqualTo(123d);
        assertThat(eval("123 #4")).isEqualTo(123d);
        assertThat(eval("123 # 4")).isEqualTo(123d);
        assertThat(eval("123##*4")).isEqualTo(123 * 4d);
        assertThat(eval("123 ## * 4")).isEqualTo(123 * 4d);
        assertThat(eval("123 #4# * 5")).isEqualTo(123 * 5d);
        assertThat(eval("123 ### * 4")).isEqualTo(123d);
        assertThat(eval("123#ABC")).isEqualTo(123d);
        assertThat(eval("123 # -|=$&/( ABC 456 ÄÖÜäöü πΩ👍")).isEqualTo(123d);
        assertThat(eval("3.14 #this is pi# * R # this is the radius used # *2    # 2*pi*r", "R", 10)).isEqualTo(3.14 * 10 * 2);
        assertThat(eval("4! #comment")).isEqualTo(4 * 3 * 2);
        assertThat(eval("4^2 #comment")).isEqualTo(4 * 4);
        assertThat(eval("2 * (3 + 4) #comment")).isEqualTo(2 * (3 + 4));
    }

}

<MSG> fix #12610: handle whitespaces in formulas (#12629)


<DFF> @@ -58,12 +58,40 @@ public class FormulaTest {
     }
 
     @Test
-    public void concatWithParenthesis() {
+    public void singleTestForDebug() {
+        assertThat(eval("A+1 4", "A", 12)).isEqualTo(134d);
+    }
+
+    @Test
+    public void concat() {
+        assertThat(eval("3A4.3", "A", 1)).isEqualTo(314.3d);
+        assertThat(eval("3A4.3", "A", 1.2)).isEqualTo(0d);
+        assertThat(eval("3A4.3", "A", 12)).isEqualTo(3124.3d);
+
+        //parenthesis
         assertThat(eval("(3(5))")).isEqualTo(35d);
         assertThat(eval("(3(5)AA)", "A", 1)).isEqualTo(3511d);
         assertThat(eval("3(5)A(A+1)", "A", 1)).isEqualTo(3512d);
+
+        //whitespaces
+        assertThat(eval("3 4")).isEqualTo(34d);
+        assertThat(eval("3 A   4.3", "A", 12)).isEqualTo(3124.3d);
+        assertThat(eval("A+1 4", "A", 12)).isEqualTo(134d);
+        assertThat(eval("3 A+1 4.3", "A", 12)).isEqualTo(3134.3d);
+        assertThat(eval("3 A+1+4.3", "A", 12)).isEqualTo(317.3d);
+        assertThat(eval("3 A+1 4.3", "A", 1.2)).isEqualTo(0d);
+
+        assertThat(eval("3 A + 1 4.3", "A", 2)).isEqualTo(334.3d);
+        assertThat(eval("3 (A+1 A + 2) 4.3", "A", 2)).isEqualTo(3344.3d);
+
+        //issue #12610
+        assertThat(eval("2+ 3 4 +5")).isEqualTo(59d);
+        assertThat(eval("2+ 3 4+5")).isEqualTo(59d);
+        assertThat(eval("1 + 2 3")).isEqualTo(33d);
+        assertThat(eval("2+(3 4)+5")).isEqualTo(41d);
     }
 
+
     @Test
     public void variableCalculations() {
         assertThat(eval("AA / 2 + B", "A", 4, "B", 5)).isEqualTo(27d);
@@ -212,6 +240,13 @@ public class FormulaTest {
         assertThat(eval("10- -+ +- -+ +-2")).isEqualTo(8);
     }
 
+    @Test
+    public void operatorPrecedenceAndEvaluationOrder() {
+        assertThat(eval("100/5/5")).isEqualTo(4);
+        assertThat(eval("100-5-5")).isEqualTo(90);
+        assertThat(eval("100-5*5")).isEqualTo(75);
+    }
+
     @Test
     public void unknownOperator() {
         assertThatThrownBy(() -> eval("2 & 5"))

fix #12610: handle whitespaces in formulas (#12629)