Files
simplexcalc/internal/calc/calc_test.go
T
clawbot a1125e59dd
check / check (push) Successful in 1m4s
Refuse numbers a double cannot hold; exact powers up to the limit (closes #3)
A power computed in float64 is refused unless its base and its result
are normal doubles, and so is a result other than zero that is not one:
below about 2.2e-308 a double keeps fewer digits, and at 0 or infinity
none. A literal that go/constant reads as 0 though it is not, such as
1e-999999999, is refused too.

A whole exponent up to 4096 either way is computed exactly and kept
when go/constant holds the result exactly, so 2^-1400 is exact.

ErrTooLarge becomes ErrOutOfRange, and its reply, "That needs a number
too large or too small for me.", is true of both.

Model: opus-5-5
2026-09-29 01:23:40 +00:00

343 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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",
})
}
// 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",
"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",
})
}
// 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",
})
}
// 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, and a result is written as a normal
// double. Anything else is refused.
func TestEvaluateOutOfRange(t *testing.T) {
t.Parallel()
expectErrors(t, map[string]error{
// Results that are not normal doubles: 2^-1074 would be written
// 5e-324.
"1e400": calc.ErrOutOfRange,
"1e300 * 1e300": calc.ErrOutOfRange,
"1e999999999 * 1e999999999": calc.ErrOutOfRange,
"1 / 1e-400": calc.ErrOutOfRange,
"2^1024": calc.ErrOutOfRange,
"2^5000": calc.ErrOutOfRange,
"(-2)^5001": calc.ErrOutOfRange,
"0.5^-5000": calc.ErrOutOfRange,
"2^-1074": calc.ErrOutOfRange,
"2^-1400": calc.ErrOutOfRange,
"-1e-310": 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,
// 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,
// 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"},
}
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)
}
}