check / check (push) Successful in 1m18s
`2^1200` was refused although the calculator computed it exactly: writing a result went through a float64 and refused anything outside a double's range. An exact result under the 4096-bit limit is now written from its exact value, rounded to 17 significant digits past that range, and a fractional power of such a number is computed by first bringing its base into range with square roots taken from the exact value. Results computed in float64 must still be normal doubles; past the 4096-bit limit is still refused. Disclosures: `1e-310` is answered again, reversing a call made in PR 11; a fractional power carries `math.Pow`'s rounding, which for a large base can reach the last digits shown. Model: opus-5-5
419 lines
15 KiB
Go
419 lines
15 KiB
Go
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",
|
||
// A fractional power of an exact number outside the range of a
|
||
// double, taken from its exact value, up to the edges of that
|
||
// range.
|
||
"(2^1200)^0.5": "4.149515568880993e+180",
|
||
"(2^1024)^0.5": "1.3407807929942597e+154",
|
||
"(2^-1200)^0.5": "2.409919865102884e-181",
|
||
"1e400^0.5": "1e+200",
|
||
"1e-400^0.5": "1e-200",
|
||
"1e-310^0.5": "1e-155",
|
||
"(2^1200)^-0.5": "2.409919865102884e-181",
|
||
"(2^1200)^0.5 / 2^600": "1",
|
||
"1e400^-0.001": "0.39810717055349726",
|
||
"1e-400^0.001": "0.39810717055349726",
|
||
"(2^2047)^0.5": "1.2711610061536464e+308",
|
||
"(2^-2044)^0.5": "2.2250738585072014e-308",
|
||
})
|
||
}
|
||
|
||
// 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 result is not a normal double,
|
||
// and so has lost digits, or all of them, whatever the size of
|
||
// the base.
|
||
"2^-1073.5 * 2^1073": calc.ErrOutOfRange,
|
||
"2^1500.5": calc.ErrOutOfRange,
|
||
"(2^1200)^0.9": calc.ErrOutOfRange,
|
||
"1e-400^0.9": calc.ErrOutOfRange,
|
||
"(2^2048)^0.5": calc.ErrOutOfRange,
|
||
"(2^-2046)^0.5": calc.ErrOutOfRange,
|
||
"(0.5^1100)^4 / (0.5^1100)^4": calc.ErrOutOfRange,
|
||
"(1/3)^1e400": 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"},
|
||
// Fractional powers of numbers just below the limit, whose bases
|
||
// take the most square roots to bring into the range of a double.
|
||
{in: "(2^-4094)^0.125", want: "8.869511863657883e-155"},
|
||
{in: "(1/3^2583)^0.5", err: calc.ErrOutOfRange},
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|