package calc_test import ( "errors" "strings" "testing" "time" "sneak.berlin/go/simplexcalc/internal/calc" ) // TestEvaluate covers the two examples the bot was specified with, and // the arithmetic around them. func TestEvaluate(t *testing.T) { t.Parallel() expectResults(t, map[string]string{ // The specification's own examples. "2 + 2": "4", "5 * 5/2": "12.5", "2+2": "4", " 7 - 10 \n": "-3", "-3 * 2": "-6", "+4": "4", "-(-4)": "4", "(1 + 2) * 3": "9", "1 + 2 * 3": "7", "((2))": "2", "8 / 2 / 2": "2", "10 - 2 - 3": "5", "7 / 2": "3.5", "25/2": "12.5", "1 / 3": "0.3333333333333333", "2 / 3": "0.6666666666666666", "0.1 + 0.2": "0.3", "1.5 * 2": "3", "2.50 * 2": "5", ".5 + .5": "1", "3. * 2": "6", "1e3 + 1": "1001", "2.5e-1": "0.25", "010 + 1": "11", "-0": "0", "0 / 5": "0", // Exact: a float64 would print 99999999980000000000. "9999999999 * 9999999999": "99999999980000000001", "1e21": "1e+21", "1e20": "100000000000000000000", "1 / 1e7": "1e-07", "1 / 1e6": "0.000001", "1234567.5": "1234567.5", "-1 / 4": "-0.25", "1e300 * 1e8": "1e+308", // Past the normal range of a double, written from the exact value // to 17 significant digits, trailing zeros dropped. "1e400": "1e+400", "1e300 * 1e300": "1e+600", "1 / 2e-400": "5e+399", "-1e-310": "-1e-310", "1 / 3e400": "3.3333333333333333e-401", }) } // TestEvaluatePowers: ^ and ** are one operator, binding tighter than // * / % and a sign on its left, and grouping to the right. func TestEvaluatePowers(t *testing.T) { t.Parallel() expectResults(t, map[string]string{ "2^3": "8", "2**3": "8", "2 ** 3 ^ 2": "512", "2^3^2": "512", "(2^3)^2": "64", "-2^2": "-4", "(-2)^2": "4", "(-2)^3": "-8", "(-2)^-3": "-0.125", "2^-1": "0.5", "2**-1": "0.5", "-2^-2": "-0.25", "2^-3^2": "0.001953125", "2 * 3^2": "18", "3^2 * 2": "18", "2^3 / 2^2": "2", "1 + 2^3 - 3^2": "0", "010^2": "100", "0.1^2": "0.01", "2^100 - 2^100 + 1": "1", "2^64": "18446744073709551616", "2^100": "1.2676506002282294e+30", "0^0": "1", "0^3": "0", "1.5^2": "2.25", "2^0.5": "1.4142135623730951", "-2^0.5": "-1.4142135623730951", "4^0.5": "2", "0^0.5": "0", "2^1023": "8.98846567431158e+307", "2^-1022": "2.2250738585072014e-308", // Past 2^53 a float64 cannot tell odd from even. "(-1)^(2^53 + 1)": "-1", "(-1)^(10^30)": "1", "(-1)^-9223372036854775808": "1", // Whole powers beyond the range of a double, held exactly. "2^-1400 * 2^1365 * 2^35": "1", "0.3^900 * 10^470": "0.25652473503365386", "2^1500 / 2^1000": "3.273390607896142e+150", "(2^1200)/(2^1199)": "2", "2^1200 % 7": "1", // Results past the range of a double, written to 17 significant // digits, up to the largest power of 2 under the 4096-bit limit. "2^1200": "1.7218479456385751e+361", "2**1200": "1.7218479456385751e+361", "2^1024": "1.7976931348623159e+308", "10^400": "1e+400", "2^-1074": "4.9406564584124654e-324", "2^-1200": "5.8077137562175032e-362", "2^-1400": "3.6141491434385841e-422", "0.5^1100": "7.3621518290228627e-332", "1.5^2000": "1.5223626185737825e+352", "(1/3)^-2000": "1.7478712517226516e+954", "2^1200 - 2^1199": "8.6092397281928753e+360", "-2^1201": "-3.4436958912771501e+361", "(-2)^1201": "-3.4436958912771501e+361", "2^2000 * 2^2000": "1.3182040934309431e+1204", "10^1232": "1e+1232", "2^4094": "2.6109722035328813e+1232", // A power computed in float64 carries its rounding into the exact // arithmetic after it, past the range of a double as within it. "2^0.5 * 1e400": "1.4142135623730951e+400", }) } // TestEvaluateModulo: % sits with * and /, left to right, and its result // takes the sign of the divisor. func TestEvaluateModulo(t *testing.T) { t.Parallel() expectResults(t, map[string]string{ "7 % 3": "1", "-7 % 3": "2", "7 % -3": "-2", "-7 % -3": "-1", "6 % 3": "0", "-6 % 3": "0", "7.5 % 2": "1.5", "0.3 % 0.1": "0", "-0.3 % 0.2": "0.1", "10 % 4 * 3": "6", "2 * 7 % 4": "2", "1 + 7 % 3": "2", "2^10 % 7": "2", "10^400 % 7": "4", "1e-30 % 1": "1e-30", "-1e-30 % 1": "1", "10 / 8 % 1": "0.25", "(7 % 3)^2": "1", "7 % (3 ^ 2)": "7", // Both operands and their quotient are held exactly, but y times // the whole part of x/y is too large to be. "(5^860*3^630/7) % (5^860/2^998/2^998)": "0.5179219763783696", // A whole number made from x^0, just below the 4096-bit limit. "(3^0 + 3^0 + 3^0)^2583 % 10": "7", }) } // expectResults checks that each expression evaluates to its result. func expectResults(t *testing.T, cases map[string]string) { t.Helper() for in, want := range cases { t.Run(in, func(t *testing.T) { t.Parallel() got, err := calc.Evaluate(in) if err != nil { t.Fatalf("Evaluate(%q) failed: %v", in, err) } if got != want { t.Errorf("Evaluate(%q) = %q, want %q", in, got, want) } }) } } // TestEvaluateRefuses covers what must be answered with an error rather // than a number. func TestEvaluateRefuses(t *testing.T) { t.Parallel() expectErrors(t, map[string]error{ "": calc.ErrNotArithmetic, " ": calc.ErrNotArithmetic, "hello": calc.ErrNotArithmetic, "/help": calc.ErrNotArithmetic, "2 +": calc.ErrNotArithmetic, "2 2": calc.ErrNotArithmetic, "2 + 2 =": calc.ErrNotArithmetic, "x + 1": calc.ErrNotArithmetic, "len(\"abc\")": calc.ErrNotArithmetic, "\"a\" + \"b\"": calc.ErrNotArithmetic, "'a' + 1": calc.ErrNotArithmetic, "2i * 2i": calc.ErrNotArithmetic, "0x10 + 1": calc.ErrNotArithmetic, "1_000 + 1": calc.ErrNotArithmetic, "1 << 10": calc.ErrNotArithmetic, "1 == 1": calc.ErrNotArithmetic, "!1": calc.ErrNotArithmetic, "func() int { return 1 }()": calc.ErrNotArithmetic, "(1 + 2": calc.ErrNotArithmetic, "1 + 2)": calc.ErrNotArithmetic, "()": calc.ErrNotArithmetic, "(2)(3)": calc.ErrNotArithmetic, "2 ^": calc.ErrNotArithmetic, "^ 2": calc.ErrNotArithmetic, "2 ^^ 3": calc.ErrNotArithmetic, "2 *** 3": calc.ErrNotArithmetic, "2 * * 3": calc.ErrNotArithmetic, "% 3": calc.ErrNotArithmetic, "50%": calc.ErrNotArithmetic, "2 × 3": calc.ErrNotArithmetic, "1 / 0": calc.ErrDivisionByZero, "1 / (2 - 2)": calc.ErrDivisionByZero, "5 / 0.0": calc.ErrDivisionByZero, "7 % 0": calc.ErrDivisionByZero, "7.5 % (1 - 1)": calc.ErrDivisionByZero, "0^-1": calc.ErrDivisionByZero, "0^-0.5": calc.ErrDivisionByZero, "(-2)^0.5": calc.ErrNoRealResult, "(-8)^(1/3)": calc.ErrNoRealResult, "(-1)^-0.5": calc.ErrNoRealResult, }) } // TestEvaluateOutOfRange: a number is held exactly, or computed in // float64 as a normal double. Anything else is refused. func TestEvaluateOutOfRange(t *testing.T) { t.Parallel() expectErrors(t, map[string]error{ // Just past the 4096-bit limit, which 2^4094 and 10^1232 are // under, and far past it. "2^4095": calc.ErrOutOfRange, "-2^4095": calc.ErrOutOfRange, "2^-4095": calc.ErrOutOfRange, "10^1233": calc.ErrOutOfRange, "2^4094 * 2": calc.ErrOutOfRange, "1e999999999 * 1e999999999": calc.ErrOutOfRange, "2^5000": calc.ErrOutOfRange, "(-2)^5001": calc.ErrOutOfRange, "0.5^-5000": calc.ErrOutOfRange, // Powers computed in float64 whose base or result is not a // normal double, and so has lost digits, or all of them. "2^-1073.5 * 2^1073": calc.ErrOutOfRange, "1e400^-0.001": calc.ErrOutOfRange, "1e-400^0.001": calc.ErrOutOfRange, "1e-310^0.5": calc.ErrOutOfRange, "(0.5^1100)^4 / (0.5^1100)^4": calc.ErrOutOfRange, "(1/3)^1e400": calc.ErrOutOfRange, "(2^1200)^0.5": calc.ErrOutOfRange, // go/constant holds numbers of this size rounded. A sum of them // can lose the answer (this one would be 0), and so can a // remainder or the sign of -1 to such a power. "7^1000 * 7^1000 + 5 - 7^1000 * 7^1000": calc.ErrOutOfRange, "7^1000 * 7^1000 / 7^1000 % 10": calc.ErrOutOfRange, "(-1)^(3^1365 * 3^1365 / 3^1365)": calc.ErrOutOfRange, "(-1)^1e1300": calc.ErrOutOfRange, "1e-1300": calc.ErrOutOfRange, "1e-1300 + 1": calc.ErrOutOfRange, "1e-700 * 1e-700": calc.ErrOutOfRange, "0.1^800 * 0.1^800": calc.ErrOutOfRange, // Both operands are held exactly, but their quotient is not. "3^1365 % 7^-1000": calc.ErrOutOfRange, // The same limit for a whole number made from x^0, which // go/constant would hold as an integer and never round, and for // a literal it reads exactly as a fraction past the limit. "(2^0 + 2^0)^4095 % 10": calc.ErrOutOfRange, "1e-1233 * 0": calc.ErrOutOfRange, // go/constant reads this literal as 0. "1e-999999999": calc.ErrOutOfRange, "1 / 1e-999999999": calc.ErrOutOfRange, }) } // expectErrors checks that each expression is refused with its error, // and never with a panic. func expectErrors(t *testing.T, cases map[string]error) { t.Helper() for in, want := range cases { t.Run(in, func(t *testing.T) { t.Parallel() got, err := calc.Evaluate(in) if !errors.Is(err, want) { t.Errorf("Evaluate(%q) = %q, %v; want error %v", in, got, err, want) } }) } } // TestEvaluateBoundsWork: computed exactly, each of these powers would // need more time and memory than any machine has. They must be answered // at once. func TestEvaluateBoundsWork(t *testing.T) { t.Parallel() cases := []struct { in string want string err error }{ {in: "9^9^9^9^9", err: calc.ErrOutOfRange}, {in: "((9^999)^999)^999", err: calc.ErrOutOfRange}, {in: "(3^2583)^4096", err: calc.ErrOutOfRange}, {in: "1.0000001^99999", want: "1.01005006557947"}, {in: "0.5^99999999999999999999", err: calc.ErrOutOfRange}, {in: "2^-9223372036854775808", err: calc.ErrOutOfRange}, {in: "(-1)^99999999999999999999", want: "-1"}, // The longest tower that fits. {in: strings.Repeat("9^", 127) + "9", err: calc.ErrOutOfRange}, // The largest power of 3 computed exactly, as often as fits. {in: "0" + strings.Repeat("*3^2583", 36), want: "0"}, // Whole numbers made from x^0, through each operation. Held as // integers, which go/constant never rounds, they would escape // the 4096-bit limit: the first needs about 69 billion bits. {in: "(((2^0+2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, {in: "(((0^0+0^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, {in: "(((-2^0-2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, {in: "((2^0+2^0)^4000*(2^0+2^0)^4000)^4096", err: calc.ErrOutOfRange}, {in: "((((2^0+2^0)/2^0)^4096)^4096)^4096", err: calc.ErrOutOfRange}, {in: "((((2^0+2^0) % 3)^4096)^4096)^4096", err: calc.ErrOutOfRange}, {in: "(((2^0+2^0)^4096)^4096)^4096 * 0", err: calc.ErrOutOfRange}, // A fraction whose numerator and denominator are both just below // the limit, and a literal whose exponent is too large to read. {in: "(3^2583/5^1760)^4096", err: calc.ErrOutOfRange}, {in: "1e99999999999999999999", err: calc.ErrOutOfRange}, // Results just below the limit, written from their exact value. {in: "2^4094", want: "2.6109722035328813e+1232"}, {in: "3^2583", want: "2.5363018640659988e+1232"}, {in: "2^-4094", want: "3.8299909843808741e-1233"}, {in: "-1/3^2583", want: "-3.9427483540814775e-1233"}, } for _, c := range cases { t.Run(c.in, func(t *testing.T) { t.Parallel() start := time.Now() got, err := calc.Evaluate(c.in) if elapsed := time.Since(start); elapsed > time.Second { t.Errorf("Evaluate(%q) took %v", c.in, elapsed) } if !errors.Is(err, c.err) || got != c.want { t.Errorf("Evaluate(%q) = %q, %v; want %q, %v", c.in, got, err, c.want, c.err) } }) } } // TestEvaluateCapsInput: the length cap is what bounds the work a // message can cause, so it must hold exactly at the boundary. func TestEvaluateCapsInput(t *testing.T) { t.Parallel() // "1+1+...+1" with the last term padded to land exactly on the cap. longest := strings.Repeat("1+", calc.MaxInputLength/2-1) + "10" if len(longest) != calc.MaxInputLength { t.Fatalf("test setup: expression is %d bytes, want %d", len(longest), calc.MaxInputLength) } got, err := calc.Evaluate(longest) if err != nil { t.Fatalf("an expression of exactly MaxInputLength bytes was refused: %v", err) } if want := "137"; got != want { t.Errorf("Evaluate(longest) = %q, want %q", got, want) } _, err = calc.Evaluate(longest + "0") if !errors.Is(err, calc.ErrTooLong) { t.Errorf("an expression over MaxInputLength gave %v, want ErrTooLong", err) } // Surrounding whitespace is not part of the expression. _, err = calc.Evaluate(" " + longest + "\n") if err != nil { t.Errorf("whitespace around a maximal expression counted against the cap: %v", err) } }