// 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 ) // A result past the normal range of a double is written to // significantDigits significant digits, the most the shortest form of a // double takes. It is rounded to them from a float of floatPrecision // bits, the bits a numerator or denominator can hold and 64 more for the // digits, so that the float rounds as the exact result would. const ( significantDigits = 17 floatPrecision = bitLimit + 64 ) // 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, written as format describes. 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), nil } // 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. Any other result in // the normal range of a double 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. Past that // range a double keeps fewer digits, or none (2^-1074 would be written // 5e-324, and 2^1024 is infinite), so such a result is written from its // exact value, to significantDigits. func format(v constant.Value) string { f, _ := constant.Float64Val(v) if constant.Sign(v) != 0 && !normal(f) { // Every number here is exact: see exact. r, _ := constant.Val(v).(*big.Rat) return new(big.Float).SetPrec(floatPrecision).SetRat(r).Text('g', significantDigits) } abs := math.Abs(f) if i := constant.ToInt(v); i.Kind() == constant.Int && abs < plainUpper { return i.ExactString() } if abs >= plainUpper || abs < plainLower { return strconv.FormatFloat(f, 'g', -1, 64) } return strconv.FormatFloat(f, 'f', -1, 64) }