check / check (push) Successful in 1m7s
The calculator now takes `^` (or `**`) for powers and `%` for modulo. Powers bind tighter than a sign on their left and group to the right, so `-2^2` is `-4` and `2^3^2` is `512`; `%` sits with `*` and `/` and takes the sign of the divisor. A small parser of our own replaces `go/parser`, which cannot express `^`; `go/constant` still computes. A whole-number exponent is exact; a fractional one is computed in float64. Every number is held as a fraction under 4096 bits, and a float64 result must be a normal double, so one message cannot stall the bot; anything else gets "That needs a number too large or too small for me." Disclosure: tiny values below about 2.2e-308, which `next` answered, are now refused. Model: opus-5-5
485 lines
13 KiB
Go
485 lines
13 KiB
Go
// Package calc evaluates the arithmetic people send the bot: decimal
|
|
// numbers, + - * / % ^, signs and parentheses.
|
|
//
|
|
// The expression is read by a small parser of its own, because Go's
|
|
// grammar has no power operator (^ is XOR there), and computed by
|
|
// go/constant, which does exact rational arithmetic: 5 * 5/2 is exactly
|
|
// 12.5, and 0.1 + 0.2 is exactly 0.3, so a result carries no binary
|
|
// floating point noise until the moment it is formatted. A power is the
|
|
// exception: one with a fractional exponent, or whose result go/constant
|
|
// cannot hold exactly, is computed in float64.
|
|
package calc
|
|
|
|
import (
|
|
"errors"
|
|
"go/constant"
|
|
"go/token"
|
|
"math"
|
|
"math/big"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// MaxInputLength caps an expression, in bytes. With maxExactExponent,
|
|
// which caps a power computed exactly, and bitLimit, which caps every
|
|
// number, it keeps a message from making the bot do unbounded work.
|
|
const MaxInputLength = 256
|
|
|
|
// bitLimit caps the numerator and denominator of every number: see
|
|
// exact.
|
|
const bitLimit = 4096
|
|
|
|
// maxExactExponent is the largest exponent, either way, of a power
|
|
// computed exactly. Past it, x^n has a numerator or denominator of more
|
|
// than 4096 bits, which go/constant holds only rounded, unless x is 0 or
|
|
// 1, and float64 computes those exactly.
|
|
const maxExactExponent = 4096
|
|
|
|
// smallestNormal is the smallest positive normal double, about 2.2e-308.
|
|
// Below it a double keeps fewer digits, down to one.
|
|
const smallestNormal = 0x1p-1022
|
|
|
|
// Results of magnitude plainUpper or more are written in exponent form
|
|
// (1e+21 rather than twenty-two digits), and so are fractions smaller
|
|
// than plainLower (1e-07 rather than 0.0000001).
|
|
const (
|
|
plainUpper = 1e21
|
|
plainLower = 1e-6
|
|
)
|
|
|
|
// The precedence of the binary operators: the higher, the tighter the
|
|
// operator binds.
|
|
const (
|
|
sumPrecedence = iota + 1
|
|
productPrecedence
|
|
powerPrecedence
|
|
)
|
|
|
|
// Errors returned by Evaluate. The bot turns each into a reply.
|
|
var (
|
|
ErrTooLong = errors.New("expression too long")
|
|
ErrNotArithmetic = errors.New("not an arithmetic expression")
|
|
ErrDivisionByZero = errors.New("division by zero")
|
|
ErrOutOfRange = errors.New("number too large or too small")
|
|
ErrNoRealResult = errors.New("no real result")
|
|
)
|
|
|
|
// decimal is the only number syntax accepted. Go's own literal syntax is
|
|
// wider, and parts of it are traps for someone typing arithmetic: 010 is
|
|
// octal 8, and 0x10, 1_000 and 1i are not what a calculator user means
|
|
// by a number. Here the x, _ or i matches no token and is refused.
|
|
const decimal = `([0-9]+\.?[0-9]*|\.[0-9]+)([eE][+-]?[0-9]+)?`
|
|
|
|
var (
|
|
// nextToken matches the token at the start of the input, after any
|
|
// whitespace: an operator, a parenthesis or a number. ** comes
|
|
// before * so that it is read as one token.
|
|
nextToken = regexp.MustCompile(`^\s*(\*\*|[-+*/%^()]|` + decimal + `)`)
|
|
|
|
// decimalLiteral matches a token that is a number.
|
|
decimalLiteral = regexp.MustCompile(`^` + decimal + `$`)
|
|
)
|
|
|
|
// Evaluate computes an arithmetic expression and returns its result as
|
|
// text: whole numbers without a decimal point, fractions in the
|
|
// shortest form that reads back as the same float64.
|
|
func Evaluate(input string) (string, error) {
|
|
s := strings.TrimSpace(input)
|
|
if len(s) > MaxInputLength {
|
|
return "", ErrTooLong
|
|
}
|
|
|
|
tokens, err := tokenize(s)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
p := parser{tokens: tokens}
|
|
|
|
v, err := p.expr(sumPrecedence)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if p.next() != "" {
|
|
return "", ErrNotArithmetic
|
|
}
|
|
|
|
return format(v)
|
|
}
|
|
|
|
// tokenize splits an expression into operators, parentheses and
|
|
// numbers, and refuses anything else. ** is returned as ^.
|
|
func tokenize(s string) ([]string, error) {
|
|
var tokens []string
|
|
|
|
for strings.TrimSpace(s) != "" {
|
|
m := nextToken.FindStringSubmatch(s)
|
|
if m == nil {
|
|
return nil, ErrNotArithmetic
|
|
}
|
|
|
|
tok := m[1]
|
|
if tok == "**" {
|
|
tok = "^"
|
|
}
|
|
|
|
tokens = append(tokens, tok)
|
|
s = s[len(m[0]):]
|
|
}
|
|
|
|
return tokens, nil
|
|
}
|
|
|
|
// parser computes an expression as it reads it, by precedence climbing:
|
|
// expr reads operands joined by operators of at least a given
|
|
// precedence, and hands the right operand of each to a deeper call that
|
|
// takes only the operators that bind tighter, so those are applied
|
|
// first.
|
|
type parser struct {
|
|
tokens []string
|
|
}
|
|
|
|
// next removes and returns the next token, or "" at the end.
|
|
func (p *parser) next() string {
|
|
tok := p.peek()
|
|
if tok != "" {
|
|
p.tokens = p.tokens[1:]
|
|
}
|
|
|
|
return tok
|
|
}
|
|
|
|
// peek returns the next token, or "" at the end, and leaves it unread.
|
|
func (p *parser) peek() string {
|
|
if len(p.tokens) == 0 {
|
|
return ""
|
|
}
|
|
|
|
return p.tokens[0]
|
|
}
|
|
|
|
// expr reads and computes an expression whose binary operators all have
|
|
// at least minPrecedence. Operators of equal precedence group to the
|
|
// left, 8/2/2 is (8/2)/2, except ^, which groups to the right: 2^3^2 is
|
|
// 2^(3^2).
|
|
func (p *parser) expr(minPrecedence int) (constant.Value, error) {
|
|
x, err := p.operand()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for {
|
|
op := p.peek()
|
|
|
|
prec := precedence(op)
|
|
if prec < minPrecedence {
|
|
return x, nil
|
|
}
|
|
|
|
p.next()
|
|
|
|
rightPrecedence := prec + 1
|
|
if op == "^" {
|
|
rightPrecedence = prec
|
|
}
|
|
|
|
y, err := p.expr(rightPrecedence)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
x, err = apply(x, op, y)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
// operand reads a number, an expression in parentheses, or a sign and
|
|
// its operand. A sign binds more loosely than a power that follows it,
|
|
// so -2^2 is -(2^2), and 2^-1 is 2^(-1).
|
|
func (p *parser) operand() (constant.Value, error) {
|
|
switch tok := p.next(); tok {
|
|
case "+", "-":
|
|
x, err := p.expr(powerPrecedence)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if tok == "-" {
|
|
x = constant.UnaryOp(token.SUB, x, 0)
|
|
}
|
|
|
|
return x, nil
|
|
case "(":
|
|
x, err := p.expr(sumPrecedence)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if p.next() != ")" {
|
|
return nil, ErrNotArithmetic
|
|
}
|
|
|
|
return x, nil
|
|
default:
|
|
return number(tok)
|
|
}
|
|
}
|
|
|
|
// precedence returns the precedence of a binary operator, and 0 for any
|
|
// other token, which ends an expression.
|
|
func precedence(op string) int {
|
|
switch op {
|
|
case "+", "-":
|
|
return sumPrecedence
|
|
case "*", "/", "%":
|
|
return productPrecedence
|
|
case "^":
|
|
return powerPrecedence
|
|
default:
|
|
return 0
|
|
}
|
|
}
|
|
|
|
func number(tok string) (constant.Value, error) {
|
|
if !decimalLiteral.MatchString(tok) {
|
|
return nil, ErrNotArithmetic
|
|
}
|
|
|
|
// Read as FLOAT, which makes every literal decimal and a fraction
|
|
// (see exact): as INT, a leading zero would make it octal.
|
|
v := constant.MakeFromLiteral(tok, token.FLOAT, 0)
|
|
|
|
// A literal such as 1e1300 or 1e-1233 is past bitLimit: see exact.
|
|
if !exact(v) {
|
|
return nil, ErrOutOfRange
|
|
}
|
|
|
|
// One too small even to be held rounded, such as 1e-999999999, is
|
|
// read as 0.
|
|
mantissa, _, _ := strings.Cut(strings.ToLower(tok), "e")
|
|
if constant.Sign(v) == 0 && strings.ContainsAny(mantissa, "123456789") {
|
|
return nil, ErrOutOfRange
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
// apply computes x op y.
|
|
func apply(x constant.Value, op string, y constant.Value) (constant.Value, error) {
|
|
var (
|
|
v constant.Value
|
|
err error
|
|
)
|
|
|
|
switch op {
|
|
case "+":
|
|
v = constant.BinaryOp(x, token.ADD, y)
|
|
case "-":
|
|
v = constant.BinaryOp(x, token.SUB, y)
|
|
case "*":
|
|
v = constant.BinaryOp(x, token.MUL, y)
|
|
case "/":
|
|
v, err = divide(x, y)
|
|
case "%":
|
|
v, err = modulo(x, y)
|
|
case "^":
|
|
v, err = power(x, y)
|
|
default:
|
|
err = ErrNotArithmetic
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !exact(v) {
|
|
return nil, ErrOutOfRange
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
func divide(x, y constant.Value) (constant.Value, error) {
|
|
// constant.BinaryOp panics on a zero divisor.
|
|
if constant.Sign(y) == 0 {
|
|
return nil, ErrDivisionByZero
|
|
}
|
|
|
|
// token.QUO divides exactly, integers included: 25/2 is 12.5.
|
|
return constant.BinaryOp(x, token.QUO, y), nil
|
|
}
|
|
|
|
// modulo computes x % y, whose result takes the sign of y, as in Python:
|
|
// -7 % 3 is 2 and 7 % -3 is -2. It is exact for decimals too: 7.5 % 2
|
|
// is 1.5.
|
|
func modulo(x, y constant.Value) (constant.Value, error) {
|
|
q, err := divide(x, y)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// The fractional part of a rounded quotient, and so the remainder,
|
|
// would be wrong.
|
|
if !exact(q) {
|
|
return nil, ErrOutOfRange
|
|
}
|
|
|
|
// x % y is y times the fractional part of x/y, which is at least 0
|
|
// and less than 1, so the result has the sign of y. It is not
|
|
// computed as x minus y times the whole part of x/y: that product
|
|
// can be too large to hold exactly when the remainder is not.
|
|
//
|
|
// For x/y = n/d the fractional part is (n mod d)/d, exact because d
|
|
// is. token.REM truncates, leaving the sign of n; adding d brings a
|
|
// negative one into range.
|
|
n, d := constant.Num(q), constant.Denom(q)
|
|
|
|
m := constant.BinaryOp(n, token.REM, d)
|
|
if constant.Sign(m) < 0 {
|
|
m = constant.BinaryOp(m, token.ADD, d)
|
|
}
|
|
|
|
return constant.BinaryOp(y, token.MUL, constant.BinaryOp(m, token.QUO, d)), nil
|
|
}
|
|
|
|
// power computes x^y. A negative x needs a whole y, and its sign is
|
|
// applied here: math.Pow would take it from the parity of y's float64
|
|
// value, and every float64 from 2^53 up is even.
|
|
func power(x, y constant.Value) (constant.Value, error) {
|
|
// n is y if y is a whole number, and Unknown otherwise.
|
|
n := constant.ToInt(y)
|
|
|
|
switch {
|
|
case constant.Sign(x) == 0 && constant.Sign(y) < 0:
|
|
return nil, ErrDivisionByZero
|
|
case constant.Sign(x) >= 0:
|
|
return nonNegativePower(x, y, n)
|
|
case n.Kind() != constant.Int:
|
|
return nil, ErrNoRealResult
|
|
}
|
|
|
|
// x is negative and n whole: x^n is (-x)^n, negated if n is odd.
|
|
v, err := nonNegativePower(constant.UnaryOp(token.SUB, x, 0), y, n)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
odd := constant.BinaryOp(n, token.AND, constant.MakeInt64(1))
|
|
if constant.Sign(odd) != 0 {
|
|
v = constant.UnaryOp(token.SUB, v, 0)
|
|
}
|
|
|
|
return v, nil
|
|
}
|
|
|
|
// nonNegativePower computes x^y for x of at least zero, and y not below
|
|
// zero if x is zero: exactly if y is a whole number n and go/constant
|
|
// holds the result exactly, otherwise in float64.
|
|
func nonNegativePower(x, y, n constant.Value) (constant.Value, error) {
|
|
e, ok := constant.Int64Val(n)
|
|
if ok && -maxExactExponent <= e && e <= maxExactExponent {
|
|
v := exactPower(x, e)
|
|
if exact(v) {
|
|
return v, nil
|
|
}
|
|
}
|
|
|
|
// y is above zero here if x is zero.
|
|
if constant.Sign(x) == 0 {
|
|
return x, nil
|
|
}
|
|
|
|
xf, _ := constant.Float64Val(x)
|
|
yf, _ := constant.Float64Val(y)
|
|
f := math.Pow(xf, yf)
|
|
|
|
// Neither x nor x^y is zero. If either is not a normal double, it
|
|
// has lost digits, or all of them.
|
|
if !normal(xf) || !normal(f) {
|
|
return nil, ErrOutOfRange
|
|
}
|
|
|
|
return constant.MakeFloat64(f), nil
|
|
}
|
|
|
|
// exactPower computes x^e by repeated squaring. x is not zero if e is
|
|
// negative. It starts from 1 as a fraction, a Float to go/constant, so
|
|
// that x^0 is a fraction like every other number (see exact). Each
|
|
// step's numbers stay small: go/constant holds one whose numerator or
|
|
// denominator reaches 4096 bits as a 512-bit float.
|
|
func exactPower(x constant.Value, e int64) constant.Value {
|
|
one := constant.MakeFloat64(1)
|
|
result := one
|
|
|
|
for n := max(e, -e); n > 0; n >>= 1 {
|
|
if n&1 == 1 {
|
|
result = constant.BinaryOp(result, token.MUL, x)
|
|
}
|
|
|
|
x = constant.BinaryOp(x, token.MUL, x)
|
|
}
|
|
|
|
if e < 0 {
|
|
result = constant.BinaryOp(one, token.QUO, result)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// exact reports whether v is a fraction whose numerator and denominator
|
|
// are both below bitLimit bits, as every number here must be, so that
|
|
// each step of arithmetic stays small. go/constant never rounds an
|
|
// integer, however large, so every number is made a fraction: literals
|
|
// are read as FLOAT, and a power starts from the fraction 1. It rounds
|
|
// a fraction that grows past the limit, to a 512-bit float and past
|
|
// that float's range to Unknown, but not one it reads from a literal,
|
|
// such as 1e-1233, so the limit is checked here.
|
|
//
|
|
// A number that is not exact is refused wherever it appears: a sum can
|
|
// lose the answer entirely (7^1000*7^1000 + 5 - 7^1000*7^1000 would be
|
|
// 0), and a remainder, or whether an exponent is whole or odd, cannot be
|
|
// read from one.
|
|
func exact(v constant.Value) bool {
|
|
r, ok := constant.Val(v).(*big.Rat)
|
|
|
|
return ok && r.Num().BitLen() < bitLimit && r.Denom().BitLen() < bitLimit
|
|
}
|
|
|
|
// normal reports whether f is a normal double, finite and at least
|
|
// smallestNormal in magnitude: a number other than zero keeps all of a
|
|
// double's digits only as one.
|
|
func normal(f float64) bool {
|
|
abs := math.Abs(f)
|
|
|
|
return abs >= smallestNormal && abs <= math.MaxFloat64
|
|
}
|
|
|
|
// format writes a result for a person to read. A whole number of
|
|
// ordinary size is written exactly, digit for digit; anything else goes
|
|
// through float64, whose shortest round-trip form is free of the noise
|
|
// (0.30000000000000004) that printing a binary fraction to a fixed
|
|
// precision produces. A result that is not zero must therefore be a
|
|
// normal double: 2^-1074 would be written 5e-324.
|
|
func format(v constant.Value) (string, error) {
|
|
f, _ := constant.Float64Val(v)
|
|
if constant.Sign(v) != 0 && !normal(f) {
|
|
return "", ErrOutOfRange
|
|
}
|
|
|
|
abs := math.Abs(f)
|
|
|
|
if i := constant.ToInt(v); i.Kind() == constant.Int && abs < plainUpper {
|
|
return i.ExactString(), nil
|
|
}
|
|
|
|
if abs >= plainUpper || abs < plainLower {
|
|
return strconv.FormatFloat(f, 'g', -1, 64), nil
|
|
}
|
|
|
|
return strconv.FormatFloat(f, 'f', -1, 64), nil
|
|
}
|